Tour

Entry into an async environment

In order to use awaitables we need to be able to co_await them, i.e. be within a coroutine.

We got four ways to achieve this:

async/main.hpp

replace int main with a coroutine

async::main co_main(int argc, char* argv[])
{
    // co_await things here
    co_return 0;
}
async/thread.hpp

create a thread for the asynchronous environments

async::thread my_thread()
{
    // co_await things here
    co_return;
}

int main(int argc, char ** argv[])
{
    auto t = my_thread();
    t.join();
    return 0;
}
async/task.hpp

create a task and run or spawn it

async::task<void> my_thread()
{
   // co_await things here
   co_return;
}

int main(int argc, char ** argv[])
{
    async::run(my_task()); // sync
    asio::io_context ctx;
    async::spawn(ctx, my_task(), asio::detached); // async, refer to async for details
    ctx.run();
    return 0;
}

Promises

Promises are the recommended default coroutine type. They’re eager and thus easily usable for ad-hoc concurrecy.

async::promise<int> my_promise()
{
   co_await do_the_thing();
   co_return 0;
}

async::main co_main(int argc, char * argv[])
{
    // start the promise here
    auto p = my_promise();
    // do something else here
    co_await do_the_other_thing();
    // wait for the promise to complete
    auto res = co_wait p;

    co_return res;
}

Tasks

Tasks are lazy, which means they won’d do anything before awaited or spwaned.

async::task<int> my_task()
{
   co_await do_the_thing();
   co_return 0;
}

async::main co_main(int argc, char * argv[])
{
    // create the task here
    auto t = my_task();
    // do something else here first
    co_await do_the_other_thing();
    // start and wait for the task to complete
    auto res = co_wait t;
    co_return res;
}

Generator

A generator is the only type in async that can co_yield values.

Generator are eager by default. Unlike std::generator the async::generator can co_await and thus is asynchronous.

async::generator<int> my_generator()
{
   for (int i = 0; i < 10; i++)
    co_yield i;
   co_return 10;
}

async::main co_main(int argc, char * argv[])
{
    // create the generator
    auto g = my_generator();
    while (g)
        printf("Generator %d\n", co_await g);
    co_return 0;
}

Values can be pushed into the generator, that will be returned from the co_yield.

async::generator<double, int> my_eager_push_generator(int value)
{
   while (value != 0)
       value = co_yield value * 0.1;
   co_return std::numeric_limits<double>::quiet_NaN();
}

async::main co_main(int argc, char * argv[])
{
    // create the generator
    auto g = my_generator(5);

    assert(0.5 == co_await g(4)); // result of 5
    assert(0.4 == co_await g(3)); // result of 4
    assert(0.3 == co_await g(2)); // result of 3
    assert(0.2 == co_await g(1)); // result of 2
    assert(0.1 == co_await g(0)); // result of 1

    // we let the coroutine go out of scope while suspended
    // no need for another co_await of `g`

    co_return 0;
}

A coroutine can also be made lazy using this_coro::initial.

async::generator<double, int> my_eager_push_generator()
{
    auto value = co_await this_coro::initial;
    while (value != 0)
        value = co_yield value * 0.1;
    co_return std::numeric_limits<double>::quiet_NaN();
}

async::main co_main(int argc, char * argv[])
{
    // create the generator
    auto g = my_generator(); // lazy, so the generator waits for the first pushed value
    assert(0.5 == co_await g(5)); // result of 5
    assert(0.4 == co_await g(4)); // result of 4
    assert(0.3 == co_await g(3)); // result of 3
    assert(0.2 == co_await g(2)); // result of 2
    assert(0.1 == co_await g(1)); // result of 1

    // we let the coroutine go out of scope while suspended
    // no need for another co_await of `g`

    co_return 0;
}

join

If multiple awaitables work in parallel they can be awaited simultaneously with join.

async::promise<int> some_work();
async::promise<double> more_work();

async::main co_main(int argc, char * argv[])
{
    std::tuple<int, double> res = async::join(some_work(), more_work());
    co_return 0;
}

select

If multiple awaitables work in parallel, but we want to be notified if either completes, we shall use select.

async::generator<int> some_data_source();
async::generator<double> another_data_source();

async::main co_main(int argc, char * argv[])
{
    auto g1 = some_data_source();
    auto g2 = another_data_source();

    int res1    = co_await g1;
    double res2 = co_await g2;

    printf("Result: %f", res1 * res2);

    while (g1 && g2)
    {
        switch(variant2::variant<int, double> nx = co_await async::select(g1, g2))
        {
            case 0:
                res1 = variant2::get<0>(nx);
                break;
            case 1:
                res2 = variant2::get<1>(nx);
                break;
        }
        printf("New result: %f", res1 * res2);
    }

    co_return 0;
}
select in this context will not cause any data loss.