Naios / continuable

C++14 asynchronous allocation aware futures (supporting then, exception handling, coroutines and connections)
https://naios.github.io/continuable/
MIT License
815 stars 44 forks source link

How to start continuable eagerly #33

Closed Gei0r closed 4 years ago

Gei0r commented 4 years ago

@Naios

TLDR: Is there a way to get eager continuable invocation for async code like in javascript?

A common use pattern of async code is this (javascript):


function backgroundWork(timeMs) {
    return new Promise(resolve => {
        console.log(`Starting background work...`);
        setTimeout(() => {
            console.log(`background work done`);
            resolve();
        }, timeMs)
    });
}

function root() {
    let promises = [];

    promises.push(backgroundWork(1000));
    promises.push(backgroundWork(2000));
    promises.push(backgroundWork(3000));

    console.log(`Now doing some expensive calculations...`);
    const start = new Date().valueOf();
    while (new Date().valueOf() < start + 5000);  // busy wait, ugh...
    console.log(`Calculations done`);

    return Promise.all(promises);
}

// main:
root().then(() => console.log(`all done`));

Output:

$ time node prDemo.js
Starting background work...
Starting background work...
Starting background work...
Now doing some expensive calculations...
Calculations done
background work done
background work done
background work done
all done
5.063 secs

Because the background work (1 s, 2 s, and 3 s) were all started before the foreground calculation (5 s), the whole program ran only 5 s.

Now I'm trying to replicate the same with C++ and continuable, using the same structure as in javascript:

#include <iostream>
#include <chrono>
#include <thread>
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>

#include <experimental/coroutine>

#define CONTINUABLE_WITH_EXPERIMENTAL_COROUTINE
#include <continuable/continuable.hpp>

namespace asio = boost::asio;

asio::io_service io;

cti::continuable<> backgroundWork(const std::chrono::duration<uint64_t> &t)
{
    using namespace std;
    using boost::system::error_code;
    return cti::make_continuable<void>([&](auto &&pr) {
        std::cout << "Starting background work..."  << std::endl;
        auto timer = std::make_shared<asio::steady_timer>(io, t);
        timer->async_wait([timer, pr=move(pr)](const error_code&) mutable {
            std::cout << "background work done" << std::endl;
            pr.set_value();
        });
    });
}

cti::continuable<> root()
{
    std::vector<cti::continuable<>> promises;

    promises.push_back(backgroundWork(std::chrono::seconds{1}));
    promises.push_back(backgroundWork(std::chrono::seconds{2}));
    promises.push_back(backgroundWork(std::chrono::seconds{3}));

    std::cout << "Now doing some expensive calculations..." << std::endl;

    // the idea is that the backgroundWork tasks should have started
    // their async operation already so that they can complete while we're
    // doing the foreground calculation.

    std::this_thread::sleep_for(std::chrono::seconds{5});
    std::cout << "Calculations done" << std::endl;

    co_await cti::when_all(promises.begin(), promises.end());
}

int main()
{
    root()
        .then([](){std::cout << "all done\n";})
        .fail([](){std::cerr << "fail\n";});

    io.run();
}

Output:

# time ./coro
Now doing some expensive calculations...
Calculations done
Starting background work...
Starting background work...
Starting background work...
background work done
background work done
background work done
all done

real    0m8.010s
user    0m0.002s
sys 0m0.000s

As you can see, the background tasks started only after the foreground work completed. As a result, the whole program required 8 s to run. This is documented in the tutorial, but is there a way to get eager evaluation like in javascript?

Naios commented 4 years ago

In that case it is not really possible to convert the execution to an eager one, however it is possible to do the following:

cti::continuable<> root()
{
  std::vector<cti::continuable<>> promises;

  promises.push_back(backgroundWork(std::chrono::seconds{1}));
  promises.push_back(backgroundWork(std::chrono::seconds{2}));
  promises.push_back(backgroundWork(std::chrono::seconds{3}));

  co_await cti::when_all(std::move(promises), cti::async([] {
    std::cout << "Now doing some expensive calculations..." << std::endl;

    // the idea is that the backgroundWork tasks should have started
    // their async operation already so that they can complete while we're
    // doing the foreground calculation.

    std::this_thread::sleep_for(std::chrono::seconds{5});
    std::cout << "Calculations done" << std::endl;
  }));
}

Which yields the following output:

Starting background work... Starting background work... Starting background work... Now doing some expensive calculations... Calculations done background work done background work done background work done all done

The trick here is that when_all requests the execution in a depth first visit way such as elements are invoked deeply after each other from left to right. Just make sure that the blocking call happens at the end because continuables that come afterwards are not requested until the thread has resumed.

What I meant in the documentation is that you always can convert the following lazy function:

continuable<> dosth() {
  return async([] {
    // do something
  });
}

To the following eager code which evenatually continues asynchronously or finishes immediately:

continuable<> dosth() {
  // do something
  return make_ready_continuable();
}

The disadvantage of the latter one is that it will be incoked immediately on creation making it impossible to make the execution deterministic (or possibly ellide it when the result is not needed anymore).

Gei0r commented 4 years ago

Could you add a method to continuable to start processing without consuming the awaitable? This way one could replicate the javascript behavior.

Naios commented 4 years ago

This is not possible with the design of this library. You would have to switch to a future implementation which is implemented on the basis of a shared state.

Naios commented 4 years ago

It seems like this issue was resolved. If you have any further questions or issue reports open a new ticket please.