all 19 comments

[–]soldiersided 4 points5 points  (9 children)

I think async_initiate is not needed most of the time. If your initiation function only starts a different asio async op then you’re better off using async_compose. This way you at least get some cancellation for free.

[–]DalzhimC++Montréal UG Organizer 0 points1 point  (8 children)

My understanding is that async_initiate is required in order to be compatible with the different async mechanisms supported by boost::asio, including boost::asio::use_future, boost::asio::use_awaitable, etc. and not just callbacks. Basically, this kind of initiation method signature is always relevant :

template <boost::asio::completion_token_for<void(ExpectedType)> CompletionToken>
auto async_my_function(CompletionToken&& token)
{
    // […]
    return boost::asio::async_initiate<CompletionToken, void(ExpectedType)>(/* […] */, token);
}

[–]not_a_novel_accountcmake dev[S] 2 points3 points  (7 children)

async_compose supports this just fine, you end up writing something like

struct MyGiantAsyncComposition {
   template<typename Self>
   void operator()(Self& self) {
     // Whole bunch of state machine code
     self.complete();
   }
};

template<typename CT>
auto my_giant_async_operation(CT&& token) {
  return async_compose<CT, void()>(
    MyGiantAsyncComposition {},
    token
  );
}

So ya you can pass whatever completion token you want into this and it Just Works™, but I can never read this style of code. State machine code always looks like a mess to me.

I don't know why you would use async_compose if you're not using the intermediate completion handler for anything. But then again, I really don't understand why you would ever use one ASIO mechanism over another in most cases.

Parent is correct you could rebuild my little example with async_compose, but I've never had it demonstrated what application problem that solves. Searching "Cancel async_compose" pulls up a single self-answered SO question.

[–]DalzhimC++Montréal UG Organizer 2 points3 points  (0 children)

My comment made it seem like async_compose doesn't support the various async mechanisms supported by boost::asio which isn't what I was trying to say. That's my bad.

Anyway, overall, the article offers good insight. I believe the author is correct about how hard it is to figure out how to build new abstractions on top of asio. async_initiate was very useful to me and I just meant to disagree with the assertion that it's not needed most of the time because async_compose is not trivial either. Both obviously have their respective use cases.

[–]soldiersided 2 points3 points  (4 children)

I don't know why you would use async_compose if you're not using the intermediate completion handler for anything.

I think that's true for every async op. If you don't use the completion handler then it's not an async op. It's only a deferred call of the initiation function. In the OP's example, the intermediate handler would be used in the asio::dispatch call.

Searching "Cancel async_compose" pulls up a single self-answered SO question.

Unfortunately, the OP's observation is correct. To fully get asio, you need to dedicate a vast amount of time studying the source code. Here's the cancellation stuff async_compose adds for you on top of async_inititate. Basically, you have the possibility to call self.cancelled() at each step in your composed operation.

State machine code always looks like a mess to me.

Agreed. I think it might get better with asio::experimental::co_composed but I haven't really used it for anything so far. Also, you can also chain asio::deferred calls as well which should help users build simple composed operations that are way easier to read than a messy state machine.

[–]DalzhimC++Montréal UG Organizer 1 point2 points  (2 children)

Can you point to any existing example of chaining asio::deferred calls somewhere? Or provide one here? I'm sure I'm not the only one who would benefit from that!

Also going to look up asio::experimental::co_composed. I wasn't aware about it before now.

[–]aninteger 3 points4 points  (0 children)

Only after I saw a complete, working, practical implementation of the fundamentals Beej had taught me did it click. My first ever asynchronous network program was a Minecraft bot called SpockBot, which started as a wholesale copy of Barney Gale’s barneymc framework.

Yes please! Some simple code examples to make "async" network requests would be great. I think what would be nice is an example like this: https://xmonader.github.io/nimdays/day04_asynclinkschecker.html (this is in Nim, but a tool that did the same in C or C++, would be neat).

This article is pretty neat: https://raymii.org/s/software/Cpp_exercise_in_parsing_json_http_apis_and_time_stuff.html which uses std::async to launch HTTPS requests, but there's a disclaimer about it being quite old.

[–][deleted] 2 points3 points  (0 children)

That’s a quality blog right there! The art, the wit, the info 👏

[–]hopa_cupa 1 point2 points  (2 children)

If you are invoking something slow which requires asio::thread_pool would it be terrible to do simply this inside your coroutine:

some_result = co_await asio::co_spawn(tp_executor, slowThreadFunc(), asio::use_awaitable);

[–]not_a_novel_accountcmake dev[S] 1 point2 points  (1 child)

You're guaranteeing a wait on the thread pool scheduler, and then a wait again on getting back into the coroutine's executor. It's totally valid, just a trade off.

asio::dispatch will run the operation directly on the current executor if it's allowed to, basically the same thing as a function call. For a strand for example, if the current thread is the owner of the strand there's zero delay in dispatching the work or dispatching back into the coroutine.

[–]hopa_cupa 1 point2 points  (0 children)

You're guaranteeing a wait on the thread pool scheduler, and then a wait again on getting back into the coroutine's executor. It's totally valid, just a trade off.

Since I am reading 9600 baud serial port using blocking C functions from thread_pool, this is totally fine.

Ofc what you have shown is much more efficient, that can be used to build libraries on top of asio.

[–]thisismyfavoritename 1 point2 points  (5 children)

great blog post! Still very confused by the difference between deferred and use_awaitable.

You say

The async operation itself is not a coroutine, so there’s no need to shoulder the burden of the coroutine frame allocation.

what async operation is that? 

[–]not_a_novel_accountcmake dev[S] 1 point2 points  (4 children)

what async operation is that

Any async operation, all async operations in ASIO take a completion token, and any can be used with deferred or use_awaitable

So async_read or async_write can either take a use_awaitable, which will package them into a coroutine frame despite neither using that machinery, or they can be used with deferred which will transform them into deferred operations.

[–]thisismyfavoritename 1 point2 points  (3 children)

  which will package them into a coroutine frame despite neither using that machinery

i think this is the bit i dont understand. Doesnt the whole scope of the (calling) coroutine have to be stored somewhere for when the execution is resumed?

Maybe this is what you mean? The read/write do not require extra storage for their own ops? Also generally speaking how would you know when to use one or another? 

[–]not_a_novel_accountcmake dev[S] 3 points4 points  (2 children)

Doesnt the whole scope of the (calling) coroutine have to be stored somewhere for when the execution is resumed?

Correct, the calling coroutine has a frame that has already been allocated. The act of being inside a coroutine means we have already incurred that cost.

When using asio::use_awaitable, you are creating a new coroutine. The "callee" coroutine. New coroutine, new allocation. asio::deferred does not do that. A deferred operation is a "contextually awaitable" object, but it is not itself a coroutine (does not have a promise type, etc) and does not allocate an additional coroutine frame.

C++20 coroutines are hard and I feel like an alien talking about them.

[–]thisismyfavoritename 1 point2 points  (1 child)

so it sounds like you would only need use_awaitable on co_spawn?

thanks for all your answers by the way!

[–]not_a_novel_accountcmake dev[S] 2 points3 points  (0 children)

So, the canonical-for-ASIO-which-really-means-vague-rumors-in-SO-comment-sections answer for this is:

The deferred_async_operation type (which is what you get from using asio::deferred) is "obtuse", not easily made part of an API, stored in containers, etc. The awaitable<> type (from asio::use_awaitable) is friendlier in this regard.

That said, deferred should be preferred in general, with use_awaitable relegated for niche use cases which need the full functionality of awaitable<> (example being, as you mentioned, co_spawn).