Giter VIP home page Giter VIP logo

cobalt's Introduction

boost.cobalt

This library provides a set of easy to use coroutine primitives & utilities running on top of boost.asio. These will be of interest for applications that perform a lot of IO that want to not block unnecessarily, yet still want to have linear & readable code (i..e. avoid callbacks).

A minimum of Boost 1.82 is necessary as the ASIO in that version has needed support. C++ 20 is needed for C++ coroutines.

Below is a showcase of features, if you're new to coroutines or asynchronous programming, please see the primer.

The assumptions are:

  • io_context is the execution_context of choice.
  • If asio::io_context is the executor, no more than one kernel thread executes within it at a time.
  • Eager execution is the way to go.
  • A thread created with promise is only using promise stuff.

Entry points

// a single threaded main running on an io_context
cobalt::main co_main(int argc, char ** argv)
{
    // wrapper around asio::steady_timer
    asio::steady_timer tim{co_await cobalt::this_coro::executor};
    dt.expires_after(std::chrono::milliseconds(100));

    co_await tim.async_wait(cobalt::use_op);
    co_return 0;
}

That is, main runs on a single threaded io_context.

It also hooks up signals, so that things like Ctrl+C get forwarded as cancellations automatically

Alternatively, run can be used manually.

cobalt::task<int> main_func()
{
    asio::steady_timer tim{co_await cobalt::this_coro::executor};
    dt.expires_after(std::chrono::milliseconds(100));

    co_await tim.async_wait(cobalt::use_op);
    co_return 0;
}


int main(int argc, char ** argv)
{
    return run(main_func());
}

Promises

The core primitive for creating your own functions is cobalt::promise<T>. It is eager, i.e. it starts execution immediately, before you co_await.

cobalt::promise<void> test()
{
    printf("test-1\n");
    asio::steady_timer tim{co_await cobalt::this_coro::executor};
    dt.expires_after(std::chrono::milliseconds(100));
    co_await tim.async_wait(cobalt::use_op);
    printf("test-2\n");
}

cobalt::main co_main(int argc, char ** argv)
{
    printf("main-1\n");
    auto tt = test();
    printf("main-2\n");
    co_await tt;
    printf("main-3\n");
    return 0;
}

The output of the above will be:

main-1
test-1
main-2
test-2
main-3

Unlike ops, returned by .wait, the promise can be disregarded; disregarding the promise does not cancel it, but rather detaches is. This makes it easy to spin up multiple tasks to run in parallel. In order to avoid accidental detaching the promise type uses nodiscard unless one uses + to detach it:

cobalt::promise<void> my_task();

cobalt::main co_main()
{
    // warns & cancels the task
    my_task();
    // ok
    +my_task();
    co_return 0;
}

Task

A task is a lazy alternative to a promise, that can be spawned onto or co_awaited on another executor.

An cobalt::task can also be used with spawn to turn it into an asio operation.

Generator

A generator is a coroutine that produces a series of values instead of one, but otherwise similar to promise.

cobalt::generator<int> test()
{
  printf("test-1\n");
  co_yield 1;
  printf("test-2\n");
  co_yield 2;
  printf("test-3\n");
  co_return 3;
}

cobalt::main co_main(int argc, char ** argv)
{
    printf("main-1\n");
    auto tt = test();
    printf("main-2\n");
    i = co_await tt; // 1
    printf("main-3: %d\n", i);
    i = co_await tt; // 2
    printf("main-4: %d\n", i);
    i = co_await tt; // 3
    printf("main-5: %d\n", i);
    co_return 0;
}
main-1
test-1
main-2
main-3: 1
test-2
main-4: 2
test-3
main-5: 3

Channels

Channels are modeled on golang; they are different from boost.asio channels in that they don't go through the executor. Instead they directly context switch when possible.

cobalt::promise<void> test(cobalt::channel<int> & chan)
{
  printf("Reader 1: %d\n", co_await chan.read());
  printf("Reader 2: %d\n", co_await chan.read());
  printf("Reader 3: %d\n", co_await chan.read());
}

cobalt::main co_main(int argc, char ** argv)
{
  cobalt::channel<int> chan{0u /* buffer size */};
  
  auto p = test(chan);
  
  printf("Writer 1\n");
  co_await chan.write(10);
  printf("Writer 2\n");
  co_await chan.write(11);
  printf("Writer 3\n");
  co_await chan.write(12);
  printf("Writer 4\n");
  
  co_await p;
  co_return 0u;
}
Writer-1
Reader-1: 10
Writer-2
Reader-1: 11
Writer-3
Reader-1: 12
Writer-4

Ops

To make writing asio operations that have an early completion easier, cobalt has an op-helper:

template<typename Timer>
struct wait_op : cobalt::op<system::error_code> // enable_op is to use ADL
{
  Timer & tim;

  wait_op(Timer & tim) : tim(tim) {}
  
  // this gets used to determine if it needs to suspend for the op
  void ready(cobalt::handler<system::error_code> h)
  {
    if (tim.expiry() < Timer::clock_type::now())
      h(system::error_code(asio::error::operation_aborted));
  }
  
  // this gets used to initiate the op if ti needs to suspend
  void initiate(cobalt::completion_handler<system::error_code> complete)
  {
    tim.async_wait(std::move(complete));
  }
};

cobalt::main co_main(int argc, char ** argv)
{
  cobalt::steady_timer tim{co_await cobalt::this_coro::executor}; // already expired
  co_await wait_op(tim); // will not suspend, since its ready
}

race

race let's you await multiple awaitables at once.

cobalt::promise<void> delay(int ms)
{
    asio::steady_timer tim{co_await cobalt::this_coro::executor};
    dt.expires_after(std::chrono::milliseconds(ms));
    co_await tim.async_wait(cobalt::use_op);
}

cobalt::main co_main(int argc, char ** argv)
{
  auto res = co_await race(delay(100), delay(50));
  asert(res == 1); // delay(50) completes earlier, delay(100) is not cancelled  
  co_return 0u;
}

cobalt's People

Contributors

akrzemi1 avatar ashtum avatar klemens-morgenstern avatar matthijs avatar ned14 avatar pdimov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cobalt's Issues

use_op causes problems with functions erased using any_completion_handler

The following code fails to build under clang-16, Linux:


#include <boost/asio/any_completion_handler.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/async_result.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/this_coro.hpp>

#include <chrono>
#include <memory>

#include "boost/async/main.hpp"
#include "boost/async/op.hpp"

namespace async = boost::async;

void async_sleep_impl(
    boost::asio::any_completion_handler<void(boost::system::error_code)> handler,
    boost::asio::any_io_executor ex,
    std::chrono::nanoseconds duration
)
{
    auto timer = std::make_shared<boost::asio::steady_timer>(ex, duration);
    timer->async_wait(boost::asio::consign(std::move(handler), timer));
}

template <typename CompletionToken>
inline auto async_sleep(
    boost::asio::any_io_executor ex,
    std::chrono::nanoseconds duration,
    CompletionToken&& token
)
{
    return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code)>(
        async_sleep_impl,
        token,
        std::move(ex),
        duration
    );
}

async::main co_main(int argc, char** argv)
{
    co_await async_sleep(co_await async::this_coro::executor, std::chrono::seconds(1), async::use_op);
    co_return 0;
}

With the following error:

[main] Building folder: async 
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/ruben/workspace/async/__build --config Debug --target all -j 6 --
[build] [ 41%] Built target boost_async
[build] Consolidate compiler generated dependencies of target main
[build] [ 52%] Built target boost_async_example_outcome
[build] [ 64%] Built target boost_async_example_delay_op
[build] [ 76%] Built target boost_async_example_echo_server
[build] [ 88%] Built target boost_async_example_delay
[build] [ 94%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[build] In file included from /home/ruben/workspace/async/main.cpp:60:
[build] /opt/boost-with-async/include/boost/asio/any_completion_handler.hpp:134:12: error: no matching constructor for initialization of 'any_completion_executor'
[build]     return any_completion_executor(std::nothrow,
[build]            ^                       ~~~~~~~~~~~~~
[build] /opt/boost-with-async/include/boost/asio/any_completion_handler.hpp:341:16: note: in instantiation of member function 'boost::asio::detail::any_completion_handler_impl<boost::async::completion_handler<boost::system::error_code>>::immediate_executor' requested here
[build]         impl)->immediate_executor(candidate);
[build]                ^
[build] /opt/boost-with-async/include/boost/asio/any_completion_handler.hpp:448:56: note: in instantiation of function template specialization 'boost::asio::detail::any_completion_handler_immediate_executor_fn::impl<boost::async::completion_handler<boost::system::error_code>>' requested here
[build]         &any_completion_handler_immediate_executor_fn::impl<Handler>,
[build]                                                        ^
[build] /home/ruben/workspace/async/include/boost/async/op.hpp:165:35: note: in instantiation of function template specialization 'boost::asio::any_completion_handler<void (boost::system::error_code)>::any_completion_handler<boost::async::completion_handler<boost::system::error_code>, boost::async::completion_handler<boost::system::error_code>>' requested here
[build]             std::move(initiation)(std::move(complete),
[build]                                   ^
[build] /home/ruben/workspace/async/main.cpp:92:12: note: in instantiation of member function 'boost::asio::async_result<boost::async::use_op_t, void (boost::system::error_code)>::op_impl<void (&)(boost::asio::any_completion_handler<void (boost::system::error_code)>, boost::asio::any_io_executor, std::chrono::duration<long, std::ratio<1, 1000000000>>), boost::asio::any_io_executor, std::chrono::duration<long, std::ratio<1, 1000000000>> &>::initiate' requested here
[build]     return boost::asio::async_initiate<CompletionToken, void(boost::system::error_code)>(
[build]            ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:45:26: note: candidate constructor not viable: no known conversion from 'decltype(associated_immediate_executor<completion_handler<error_code>, any_io_executor>::get(t, ex))' (aka 'boost::async::detail::completion_handler_noop_executor') to 'const any_completion_executor' for 2nd argument
[build] any_completion_executor::any_completion_executor(std::nothrow_t,
[build]                          ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:58:26: note: candidate constructor not viable: no known conversion from 'decltype(associated_immediate_executor<completion_handler<error_code>, any_io_executor>::get(t, ex))' (aka 'boost::async::detail::completion_handler_noop_executor') to 'any_completion_executor' for 2nd argument
[build] any_completion_executor::any_completion_executor(std::nothrow_t,
[build]                          ^
[build] /opt/boost-with-async/include/boost/asio/any_completion_executor.hpp:101:3: note: candidate template ignored: substitution failure [with OtherAnyExecutor = nothrow_t]: no type named 'type' in 'boost::asio::constraint<false>'
[build]   any_completion_executor(OtherAnyExecutor e,
[build]   ^
[build] /opt/boost-with-async/include/boost/asio/any_completion_executor.hpp:125:3: note: candidate template ignored: substitution failure [with OtherAnyExecutor = decltype(associated_immediate_executor<completion_handler<error_code>, any_io_executor>::get(t, ex))]: no type named 'type' in 'boost::asio::constraint<false>'
[build]   any_completion_executor(std::nothrow_t, OtherAnyExecutor e,
[build]   ^
[build] /opt/boost-with-async/include/boost/asio/any_completion_executor.hpp:158:3: note: candidate template ignored: substitution failure [with Executor = nothrow_t]: no type named 'type' in 'boost::asio::constraint<false>'
[build]   any_completion_executor(Executor e,
[build]   ^
[build] /opt/boost-with-async/include/boost/asio/any_completion_executor.hpp:180:3: note: candidate template ignored: substitution failure [with Executor = decltype(associated_immediate_executor<completion_handler<error_code>, any_io_executor>::get(t, ex))]: no type named 'type' in 'boost::asio::constraint<false>'
[build]   any_completion_executor(std::nothrow_t, Executor e,
[build]   ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:34:26: note: candidate constructor not viable: requires 1 argument, but 2 were provided
[build] any_completion_executor::any_completion_executor(nullptr_t) BOOST_ASIO_NOEXCEPT
[build]                          ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:39:26: note: candidate constructor not viable: requires single argument 'e', but 2 arguments were provided
[build] any_completion_executor::any_completion_executor(
[build]                          ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:52:26: note: candidate constructor not viable: requires single argument 'e', but 2 arguments were provided
[build] any_completion_executor::any_completion_executor(
[build]                          ^
[build] /opt/boost-with-async/include/boost/asio/impl/any_completion_executor.ipp:29:26: note: candidate constructor not viable: requires 0 arguments, but 2 were provided
[build] any_completion_executor::any_completion_executor() BOOST_ASIO_NOEXCEPT
[build]                          ^
[build] 1 error generated.
[build] gmake[2]: *** [CMakeFiles/main.dir/build.make:76: CMakeFiles/main.dir/main.cpp.o] Error 1
[build] gmake[1]: *** [CMakeFiles/Makefile2:161: CMakeFiles/main.dir/all] Error 2
[build] gmake: *** [Makefile:136: all] Error 2
[proc] The command: /usr/bin/cmake --build /home/ruben/workspace/async/__build --config Debug --target all -j 6 -- exited with code: 2
[driver] Build completed: 00:00:02.542
[build] Build finished with exit code 2

wait_group reference

https://klemens.dev/async/#wait_group

  • Having it be a struct looks weird
  • __wait_one_op__, __wait_op__ look like unrendered conent
  • // wait for one tasks to complete syntax error, should be "wait for one task"
  • // swallow the exception here, and wait for the completion I don't understand what does this function do
  • // wait for one tasks to complete This is missing some details - It waits for a task to complete and removes it from the group once it's completed. The returned awaitable holds a reference to the wait_group internal data, so it has view semantics. That should be documented for every function with such semantics.
  • std::size_t size() const; Specify here that completed tasks count towards the wait group size until they are removed, either via one of the wait functions, or explicitly via reap().
  • What's the behavior if a task fails or is cancelled? Does wait_one / wait_all remove it? I've got the feeling this is related to the constructor parameter, but it's not evident.

Multiple `mismatched-new-delete` warnings with gcc 11.3 and -Wall

When building latest master (53fded2) with gcc 11.3 (Ubuntu 22.04) and -Wall, the compiler complains about mismatched new-delete warnings. See snippet below:

/home/jfp/async/test/async_for.cpp: In function ‘boost::async::generator<int> test_data_gen()’:
/home/jfp/async/test/async_for.cpp:30:1: warning: ‘static void boost::async::promise_memory_resource_base::operator delete(void*, std::size_t)’ called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
   30 | }

Is this warning safe to ignore?

thread support

The library is currently strictly single threaded, and the former async::thread class has been removed. If we want multi-threading we need would need synchronization primitives, such as mutex etc. (the sam stuff, but simpler).

Potentially also thread-safe channels.

clang complains about unknown warning group

[build] In file included from /home/ruben/workspace/async/include/boost/async/io/resolver.hpp:11:
[build] /home/ruben/workspace/async/include/boost/async/io/endpoint.hpp:35:32: warning: unknown warning group '-Wsubobject-linkage', ignored [-Wunknown-warning-option]
[build] #pragma GCC diagnostic ignored "-Wsubobject-linkage"

Note that clang may define __GNUC__

Tutorial > echo server > before listen function

This is a coroutine that can be co_awaited multiple times, until co_return.

Seems to leave the sentence mid-way, suggested:

This is a coroutine that can be co_awaited multiple times, until a co_return statement is reached.

Documentation typos & syntax errors

** Location: https://klemens.dev/async/#design:concepts**

This library exposed a set of

This library exposes ...

** Location: all document **

the select [verb]

Should be "the select function [verb]"

** Location: https://klemens.dev/async/#select **

The select function can be used to co_await one Awaitables

The select function can be used to co_await one Awaitable

** Location: https://klemens.dev/async/#design:select **

The select must however wait for all awaitable to complete once it initiates the awaiting operation.

This is confusing, since in the line just before, it's stated that

It awaits multiple awaitables in a pseudo-random order and will return the result of the first one to complete.

If I've understood it correctly, I'd suggest something like:

"select waits for any of the passed-in awaitables to complete, and returns a variant holding the alternative corresponding to the completed variant. It then interrupts the other awaitables using the interrupt_wait mechanism (link)."

** Location: https://klemens.dev/async/#left_select **

left_select gets evaluated strictly from left to right, i.e. the left-most awaitable that is ready will be used. This can lead to starvation which is why async/select.hpp should be considered as a sound default.

Should state that this consideration only applies when more than one awaitable is already ready.

** Location: https://klemens.dev/async/#gather, https://klemens.dev/async/#join **

The gather function can be used to co_await multiple Awaitables at once with properly connected cancellations.

What does "properly connected cancellations" mean? Does this get guaranteed for all async types? If that's the case, move this statement below and specify that this consideration applies only if you're writing your own awaitables.

** Location: https://klemens.dev/async/#promise **

That is, it cannot be resumed.

Suggestion: That is, it cannot co_yield and be resumed later.

** Location: https://klemens.dev/async/#promise **

By using the task gets detached. Without the it would generate a nodiscard warning.

Unrendered text regarding +

** Location: https://klemens.dev/async/#promise **

The concept "attached/detached" is not explained anywhere. By reading the code, looks like "an attached promise gets cancelled when destroyed, while a detached promise is not".

** Location: https://klemens.dev/async/#promise **

bool attached(); missing const specifier

** Location: https://klemens.dev/async/#promise **

This allows to spawn promised with a simple +, e.g. +my_task().

This allows to spawn a detached promise using +my_task().

** Location: https://klemens.dev/async/#promise **

This allows code like while (p) co_await p:

C++ syntax error.

** Location: https://klemens.dev/async/#promise **

The thread promise has the following properties.

You haven't explained what a "thread promise" is.

** Location: https://klemens.dev/async/#generator **

A generator is an eager coroutine that can not only be resumed but yield values to the caller.

A generator is an eager coroutine that can be resumed and yield values to the caller.

** Location: https://klemens.dev/async/#generator **

Values can be pushed into the generator, when Push is set to non-void:

The declaration of generator is not there, so no context about Push is present. Suggestion:

"Values can be pushed into the generator by specifying a second, non-void template argument to generator."

** Location: https://klemens.dev/async/#generator **

This will cause the generator to post it’s own ...

Should be "its"

** Location: https://klemens.dev/async/#generator **

auto operator()( Push && push);
auto operator()(const Push & push);

Document what does this auto translate into.

** Location: https://klemens.dev/async/#generator **

this is inefficient for synchronous generators.

Document what is a "synchronous generator" (I thought they were all asynchronous)

** Location: https://klemens.dev/async/#task **

// enable co_await only for rvalues.
auto operator co_await () **;

Should be &&

** Location: https://klemens.dev/async/#detached **

The type is a template in the docs but not in the actual code.

** Location: https://klemens.dev/async/#detached **

async::main co_main(int argc, char *argv[])
{
delay(std::chrono::milliseconds(50));
co_return 0;
}

Seems to not use the function that's being developed (delayed_print)

** Location: https://klemens.dev/async/#detached **

This allows code like while (p) co_await p:

C++ syntax error

** Location: https://klemens.dev/async/#detached **

// enable co_await.
auto operator co_await ();

Extra backticks

** Location: https://klemens.dev/async/#async_operation **

async_operation doesn't exist in code

** Location: https://klemens.dev/async/#channel **

// read a value to a channel
read_op read();
// write a value to the channel
write_op write(const T && value);
write_op write(const T & value);
write_op write( T && value);
write_op write( T & value);
// write a value to the channel if T is void
write_op write();

Unrendered types

** Location: https://klemens.dev/async/#channel **

Channels are a tool to have two coroutines communicate and synchronize

Missing period.

** Location: https://klemens.dev/async/#channel **

A channel type be void, in which case write takes no parameter.

A channel type can ...

** Location: https://klemens.dev/async/#with **

async::promise work(my_resource & res;

C++ syntax error.

** Location: https://klemens.dev/async/#with **

The teardown can either be done by providing an exit member function or a tag_invoke function that returns an Awaitable, .

Extra trailing comma.

** Location: https://klemens.dev/async/#with **

Please document that the std::exception_ptr is nullptr if the scope exit happens without an active exception.

use_op incorrectly moves lvalue reference arguments passed to async_initiate

This statement

// ws is a beast::websocket::stream, request is an http::request<http::string_body>
co_await ws.async_accept(request, as_tuple(use_op));

Leaves ws in an invalid state by moving the websocket stream's implementation, which is a shared_ptr passed by lvalue reference to asio::async_initiate.

The problem is in op.hpp, within these lines:

  template <typename Initiation, typename... InitArgs>
  static auto initiate(Initiation && initiation,
                       boost::async::use_op_t,
                       InitArgs &&... args)
      -> op_impl<std::decay_t<Initiation>, std::decay_t<InitArgs>...>
  {
    return op_impl<std::decay_t<Initiation>, std::decay_t<InitArgs>...>(
        std::forward<Initiation>(initiation),
        std::forward<InitArgs>(args)...);
  }

This decay will end up moving lvalue references. Comparing this to other Asio's completion tokens, this should be:

  template <typename Initiation, typename... InitArgs>
  static auto initiate(Initiation initiation, boost::async::use_op_t,
                       InitArgs... args) -> op_impl<Initiation, InitArgs...> {
    return op_impl<Initiation, InitArgs...>(std::move(initiation),
                                            std::move(args)...);
  }

With this change, the code works.

Coroutine primer > Event loop typo

Since the coroutines in async can co_await events, they need to be run on an event-loop. That is another piece of code is responsible for tracking outstanding events and resume a co_await coroutine

Suggestion: "This is another piece of code responsible for tracking outstanding events and resuming coroutines co_await-ing them.

Easy to use support for Boost.Coroutine2 suspend-resumption

Down the road, after this review, it would be great if Async gained integration with Boost.Coroutine2 such that:

  1. C++ coroutine with Async calls C function using a Coroutine2 stack.
  2. C function calls a C callback.
  3. C callback suspends execution of the Async C++ coroutine.
  4. When Async C++ coroutine resumes, Coroutine2 stack is restored, and the C callback returns.

Then you get easy to use integration between Async stackless code and stackful coroutine code, and everybody gets to party!

Docs: recommended changes

The following suggestions for changes in the documentation come from a perspective of someone who is not very well familiar with asynchronity, C++ coroutines, or Node.js/Python, someone who also wants to learn something about asynchronity.

  • Following the advice from Robert Ramey, the first thing that the docs should say, in very brief, is what this library is for, so that I can make a call whether I will need it, and if it is worth investing the next 15 minutes in reading the library overview. Currently the docs start with a warning about MSVC compilers and from the tables representing parts of the library. This information, useful as it is, should come a bit later. Niall Douglas made an introduction in the Boost Review announcement, and maybe you want to reuse parts of it. He mentions terms "coroutine support library with excellent support for foreign asynchronous design patterns".
  • It looks like the introduction assumes that the reader is already familiar with the Node.js or Python equivalents.
  • The introduction seems to use terms "concurrent" and "asynchronous" interchnageably, but I do not think they are. The former stresses the fact that one task doesn't need to finish bofore another starts. the latter stresses the control flow: one operation is not interested in the results of another operation that is being launched. I think your library enables both single-threades concurrency (generator) and single-threaded asynchrony (task, promise).
  • Does this mean this library cannot be used for multi-threaded asynchrony? For instance when I want more than one thread to process the ASIO's task queue?
  • In the Overview we have "Table 1. Coroutine types". But is this name correct? I do not think promise or generator are coroutine types or coroutines. They are types that help define when coroutine suspends and what interface is exposed to the caller. But "coroutine" in C++ nomenclature is the body of the function inside the curly braces. I know there is no good term for that. Maybe use "Coroutine Interface Types".
  • Mention in the initial sections that this library depends on Boost.ASIO (if it does), or that parts of it do not depend on Boost.ASIO (I can see some ASIO headers beeing #included)
  • Document if the familiarity with Boost.ASIO is necessary to understand these docs, and if using this library without Boost.ASIO makes sense.

Integration into Asio

Is the plan to merge into Asio ? Also, thanks for this! Library coroutines couldn't come sooner :)

slice algo

A possible algo akin to select could be slice (insert better name here) that does a random (or left-to-right for left_slice) check of awaitables, and stops once it finds one that's ready / completes immediately.

it would return a tuple/range of optionals or a bitset/vector if all are void.

This would not require interruption.

A variant on this algorithm could be an algo that doesn't wait but just returns those ready/completed immediatly and interrupts/completes everything else.

@madmongo1

async::with should be able to return a value

This code is a little bit verbose:

    error_code ec;

    // Launch a Redis client in the background. Cancel it on exit
    co_await boost::async::with(redis_runner(st), [&](redis_runner&) -> promise<void> {
        // Run the listening loop
        ec = co_await run_listener(listening_endpoint, st);
    });

    co_return ec;

Would be great like:

    // Launch a Redis client in the background. Cancel it on exit
    co_return co_await boost::async::with(redis_runner(st), [&](redis_runner&) -> promise<error_code> {
        // Run the listening loop
        co_return co_await run_listener(listening_endpoint, st);
    });

[feature] .then() method for promise

Being able to attach a callback to the promise type which detaches the promise and gets called when done (given an exception_ptr and the return value) would allow the user to use the promise type in non-coroutine code in traditional async callback fashion, and allow one to use the promise type as a generic handle for async results.

Example usage:

async::promise<int> foo()
{
    co_return 1;
}
foo().then([](std::exception_ptr, int) {
    // Do something with I
});

This is similar to Promise.prototype.then() in JS.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

[feature] Immediate promise

It might be possible to create a promise that's immediate & never allocates, e.g.:

promise<int>::now(42);

This does however seem like it should be handled by the optimizer, some time in the future, so I don't know if it's worth bothering with that to avoid a single pooled allocation.

channel example not working

Hi,

I am playing around with this library and it seems useful for me. 👍

The example given in the channel.adoc didn't work, after adjusting it to:

async::promise<void> producer(channel<int> & chan)
{
  for (int i = 0; i < 4; i++)
    co_await chan.write(i);
  chan.close();
}

async::main co_main(int argc, char * argv[])
{
  async::channel<int> c{1U, co_await async::this_coro::executor}; // added 1U as first parameter.
  auto w = producer(chan); // added

  while (c.is_open())
    std::cout << co_await c.read() << std::endl;

  co_return 0;
}

Tutorial > delay typo

Return a value that gets returns from the implicit main.

Should be "Return a value from the implicit main."

This coroutine then has an executor in it’s promise

Should be "its promise"

Suggestion: Health-check example

Async could provide a health check as an example since it is simple enough to implement and useful in general. Constraints

  • Ping the server periodically (can be a http endpoint).
  • Use only one timer.
  • The timer should not be cancelled.
  • On expiration the timer should exit with success.

The interface could look like

co_await health_check(connection, std::chrono::seconds{1});

Tutorial > echo server > exception handling

The operation is wrapped a try-catch block, because it should just stop & close the socket for any error (including cancellation).

I'd rephrase to

When using the use_op completion token, I/O errors are translated into C++ exceptions. Additionally, if the coroutine gets cancelled (e.g. because the user hit Ctrl-C), an exception will be raised, too. Under these conditions, we print the error and exit the loop.

detached example doesn't seem to work

#include <boost/asio/steady_timer.hpp>
#include <boost/async/main.hpp>

#include <cstdio>

#include "boost/async/detached.hpp"
#include "boost/async/op.hpp"

namespace async = boost::async;
namespace asio = boost::asio;

async::detached delayed_print(std::chrono::milliseconds ms)
{
    asio::steady_timer tim{co_await async::this_coro::executor, ms};
    co_await tim.async_wait(async::use_op);
    printf("Hello world\n");
}

async::main co_main(int argc, char* argv[])
{
    delayed_print(std::chrono::milliseconds(50));
    co_return 0;
}

Under clang-16/Ubuntu, fails with

[build] /home/ruben/workspace/async/main.cpp:12:17: error: implicit instantiation of undefined template 'boost::async::promise<void>'
[build] async::detached delayed_print(std::chrono::milliseconds ms)

Upon adding the relevant include, keeps failing with

[build] /home/ruben/workspace/async/main.cpp:13:17: error: no viable conversion from returned value of type 'promise<void>' to function return type 'async::detached'
[build] async::detached delayed_print(std::chrono::milliseconds ms)

These snippets must be built as part of the library to prevent code rotting.

Motivation section typos

[...] like boost.beast, `boost.mysql or boost.redis.

Should be Boost.Beast, Boost.Mysql, ...
There is an extra backtick before MySQL.

Tutorial > echo server > run_server typo

We do not need to do the same for the listener, because it will just stop on it’s own, when l gets destroyed. The destructor of a generator will cancel it.

Should be "its own"

Jamfile should require C++20

Since the required C++ standard is high, the Jamfile should enforce it. You currently got a bunch of long errors if not specifying cxxstd=20.

This is likely going to break the Boost build for cxx < 20 if not handled correctly.

Tutorial > echo server link to relevant Asio docs

We’ll be using the use_op token everywhere, so we’re using a default completion token, so that we can skip the last parameters.

Motivation section - comparison with Asio needs rewording

Unlike asio::awaitable and asio::experimental::coro, async coroutines are open. That is, an asio::awaitable can only await other asio::awaitable and does not provide coroutine specific synchronization mechanisms. async on the other hand provices channel and different wait types (select, gather etc.) that are optimized for awaitables.

  • From this sentence, I get that async coroutines can await asio::awaitables - if it's not the case, a rewording on what "open" means is needed.
  • Typo: provices
  • One may argue that Asio does provide channel and parallel_group. I'd reword as something like:

Compared to Asio, async provides synchronization primitives that are easier to use, more feature-rich and more optimized. It can do so because it only targets coroutines, while Asio needs to support a broad range of completion tokens.

wait_group doesn't build under gcc-12

The following code:

#include <boost/async/promise.hpp>
#include <boost/async/wait_group.hpp>

static boost::async::promise<void> f()
{
    boost::async::wait_group gp;
    co_return;
}

Doesn't build under GCC12, Linux, Debug. It fails with the following error:

[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/select.hpp: In instantiation of ‘static boost::async::detail::fork boost::async::detail::select_ranged_impl<Ct, URBG, Range>::awaitable::await_impl(boost::async::detail::select_ranged_impl<Ct, URBG, Range>::awaitable&, std::size_t) [with boost::asio::cancellation_type Ct = boost::asio::cancellation_type::all; URBG = std::mersenne_twister_engine<long unsigned int, 32, 624, 397, 31, 2567483615, 11, 4294967295, 7, 2636928640, 15, 4022730752, 18, 1812433253>&; Range = std::__cxx11::list<boost::async::promise<void> >&; std::size_t = long unsigned int]’:
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/select.hpp:464:31:   required from ‘bool boost::async::detail::select_ranged_impl<Ct, URBG, Range>::awaitable::await_ready() [with boost::asio::cancellation_type Ct = boost::asio::cancellation_type::all; URBG = std::mersenne_twister_engine<long unsigned int, 32, 624, 397, 31, 2567483615, 11, 4294967295, 7, 2636928640, 15, 4022730752, 18, 1812433253>&; Range = std::__cxx11::list<boost::async::promise<void> >&]’
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/wait_group.hpp:38:34:   required from here
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/select.hpp:385:25: error: ‘operator new’ is provided by ‘std::__n4861::__coroutine_traits_impl<boost::async::detail::fork, void>::promise_type’ {aka ‘boost::async::detail::fork::promise_type’} but is not usable with the function signature ‘static boost::async::detail::fork boost::async::detail::select_ranged_impl<Ct, URBG, Range>::awaitable::await_impl(boost::async::detail::select_ranged_impl<Ct, URBG, Range>::awaitable&, std::size_t) [with boost::asio::cancellation_type Ct = boost::asio::cancellation_type::all; URBG = std::mersenne_twister_engine<long unsigned int, 32, 624, 397, 31, 2567483615, 11, 4294967295, 7, 2636928640, 15, 4022730752, 18, 1812433253>&; Range = std::__cxx11::list<boost::async::promise<void> >&; std::size_t = long unsigned int]’
[build]   385 |     static detail::fork await_impl(awaitable & this_, std::size_t idx)
[build]       |                         ^~~~~~~~~~
[build] In file included from /opt/boost-1.83-dev-with-redis/include/boost/async/gather.hpp:12,
[build]                  from /opt/boost-1.83-dev-with-redis/include/boost/async/detail/wait_group.hpp:13,
[build]                  from /opt/boost-1.83-dev-with-redis/include/boost/async/wait_group.hpp:8,
[build]                  from /home/ruben/workspace/servertech-chat/server/src/listener.cpp:9:
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/gather.hpp: In instantiation of ‘static boost::async::detail::fork boost::async::detail::gather_ranged_impl<Range>::awaitable::await_impl(boost::async::detail::gather_ranged_impl<Range>::awaitable&, std::size_t) [with Range = std::__cxx11::list<boost::async::promise<void> >&; std::size_t = long unsigned int]’:
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/gather.hpp:322:33:   required from ‘bool boost::async::detail::gather_ranged_impl<Range>::awaitable::await_ready() [with Range = std::__cxx11::list<boost::async::promise<void> >&]’
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/wait_group.hpp:90:34:   required from here
[build] /opt/boost-1.83-dev-with-redis/include/boost/async/detail/gather.hpp:281:25: error: ‘operator new’ is provided by ‘std::__n4861::__coroutine_traits_impl<boost::async::detail::fork, void>::promise_type’ {aka ‘boost::async::detail::fork::promise_type’} but is not usable with the function signature ‘static boost::async::detail::fork boost::async::detail::gather_ranged_impl<Range>::awaitable::await_impl(boost::async::detail::gather_ranged_impl<Range>::awaitable&, std::size_t) [with Range = std::__cxx11::list<boost::async::promise<void> >&; std::size_t = long unsigned int]’
[build]   281 |     static detail::fork await_impl(awaitable & this_, std::size_t idx)

It does build under clang-17.

noexcept mode

It would be possible to add a noexcept mode, that turns everything into a system::result with error_codes.
The issue will however be allocation failure, which makes the design a bit more tricky.

Additionally, this library relies on asio, so I couldn't convince myself yet that this is a good match for this library and shouldn't be a fork, like embedded-async.

coro_deleter

make the coro_deleter public as a public async::unique_handle.

Tutorial > echo server > run_server > cancellation

Constructor the listener (destruction will cause cancellation!)

Seems to be syntax-incorrect. I'd suggest to reword it, too:

Construct the listener generator coroutine. When the object is destroyed, the coroutine will be cancelled, performing all required cleanup.

Benchmark position in docs looks weird

"Benchmarks" come first, even before you explain the basics. A user getting to this section lacks the knowledge to get anything useful from it. I'd move it down, probably to the very end of the document.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.