On Sun, Aug 13, 2023 at 3:46 PM Marcelo Zimbres Silva via Boost
Hi, this is my review of the proposed Boost.Async. Before I issue my final decision (ACCEPT/REJECT) I would like to ask the author some questions.
Thanks for the review and letting me address those questions first!
Q1: Default completion token for other Boost libraries ================================================================
I guess it will be a very common thing for all apps using Boost.Async to have to change the default completion token to use_op or use_task. Like you do in the examples
using tcp_acceptor = async::use_op_t::as_default_on_ttcp::acceptor; using tcp_socket = async::use_op_t::as_default_on_ttcp::socket;
Could this be provided automatically, at least for other Boost libraries? I know this would be a lot of work, but it would make user code less verbose and users would not have to face the token concept at first.
If could be, but I am also experimenting with how an async.io library could look. I.e. one that is async only (co_await stream.read()) and banished the asio complexity to the translations units. You can look at my experiments here https://github.com/klemens-morgenstern/async/pull/8
Q2: Lazy vs Eager ================================================================
It’s an eager coroutine and recommended as the default;
Great, we can experiment with eagerness and laziness. But why is an eager coroutine recommended as default?
Because that's usually what I would want. If I have a coroutine `async_do_the_thing()` I would expect it to do the thing even if I don't await the result. I think this is intuitive especially for those unfamiliar with asio.
Q3: async_ready looks great ================================================================
We can however implement our own ops, that can also utilize the async_ready optimization. To leverage this coroutine feature, async provides an easy way to create a skipable operation:
I think I have a use case for this feature: My first implementation of the RESP3 parser for Boost.Redis was based on Asio's async_read_until, which like every Asio async function, calls completion as if by post. The cost of this design is however high in situations where the next \r\n delimiter is already in the buffer when async_read_until is called again. The resulting rescheduling with post is actually unnecessary and greatly impacts performance, being able to skip the post has performance benefits. But what does *an easy way to create a skipable operation* actually mean? Does it
- avoid a suspension point? - avoid a post? - act like a regular function call?
Theren are two mechanisms at work here: - if you provide a ready() function in a custom op, it will avoid suspension altogether, thus be like a normal function call - if immediate completion is awaitable, the coroutine will suspend, but resume rightaway. Thus avoid a post.
Q4: Synchronization primitives ================================================================
Will this library ever add synchronization primitives like async mutexes, condition variables, barriers etc. Or are they supposed to be used from an external library like proposed Boost.Sem.
It has channels that can do the mutex & barrier. I don't like using names like mutex et al., because they imply they're thread safe. Since async is single-threaded you can actually use std::mutex if you need to cross threads btw. If we only have one mechanism, channels are the most versatile, but I think there is indeed a need for something condition variable like. I just don't know what it is yet, I am thinking maybe a pub/sub utility, like a multicast-channel or something might do the trick. I would however like to base this on user experience. The channel model has proven itself in other languages, so I am confident enough.
Q5: Boost.Async vs Boost.Asio ================================================================
I use C++20 coroutines whenever I can but know very little about their implementation. They just seem to work in Asio and look very flexible with use_awaitable, deferred, use_promise and use_coro. What advantage will Boost.Async bring over using plain Asio in regards to coroutine?
It's open to any awaitable, i.e. a user can just co_await whatever he wants. asio prevents this by design because it has way less it can assume about the environment. That is, asio::awaitable cannot await anything other than itself and an async op, not even asio::experimental::coro. Furthermore all of those are meant for potentially threaded environment so everything they do needs to be an async_op, i.e. operator|| internally does multiple co_spawns through a `parallel_group`. Because async can assume more things about how it's run, it can provide a loss-less select, which `operator||` cannot do. Likewise the channels work (mostly) without post, because they just switch from one coro to the other, whereas asio's channels need to use posting etc. So in short: it's more efficient because it's optimized for single threaded environments it gives you better synchronization mechanisms and better extensibility. I don't see the asio coroutines as competition, they just solve a different use-case. If you were to ask me which to use in boost.redis, I'd tell you to go for asio's. But if you wanted to write a small chat server based on boost libraries, I'd recommend async.
NOTE1 ================================================================
Please state your experience with C++ coroutines and ASIO in your review, and how many hours you spent on the review.
I have spent a bit more than a day reading the docs, writing the review and integrating Boost.Redis.
My experience with C++20 coroutines is limited to using them with Asio.
I would also like to thank Klemens for submitting Boost.Async and Niall for offering to be its review manager.
Marcelo
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost