ASIO: Writing Composed Operations
Hi, I was looking at https://www.boost.org/doc/libs/develop/libs/beast/doc/html/beast/using_io/wr... and came up with a question I can't answer: In https://github.com/boostorg/beast/blob/develop/example/echo-op/echo_op.cpp#L... why does it use the AsyncStream associated executor without even looking at the potentially different (right?) Handler associated executor? Regards.
The executor_work_guard is required for situations in which the IoExecutor (Executor associated with an IoObject) has different identity (or type) than the Executor associated with the CompletionHandler (e.g. when used with asio::use_future_t). Since this is not a "primitive" async operation (that calls directly into the OS and manages operation suspension), we don't need to maintain an executor_work_guard for the CompletionHandler's executor. Currently, the IoExecutor is always io_context::executor_type, however there is a proposal (https://wg21.link/p1322r0) to enable users to provide custom Executors to IoObjects. On Fri, Nov 9, 2018 at 11:00 AM Cristian Morales Vega via Boost-users < boost-users@lists.boost.org> wrote:
Hi,
I was looking at
https://www.boost.org/doc/libs/develop/libs/beast/doc/html/beast/using_io/wr... and came up with a question I can't answer: In
https://github.com/boostorg/beast/blob/develop/example/echo-op/echo_op.cpp#L... why does it use the AsyncStream associated executor without even looking at the potentially different (right?) Handler associated executor?
Regards. _______________________________________________ Boost-users mailing list Boost-users@lists.boost.org https://lists.boost.org/mailman/listinfo.cgi/boost-users
On Fri, 9 Nov 2018 at 11:38, Damian Jarek via Boost-users
The executor_work_guard is required for situations in which the IoExecutor (Executor associated with an IoObject) has different identity (or type) than the Executor associated with the CompletionHandler (e.g. when used with asio::use_future_t).
Don't really know why if they are equal the executor_work_guard is not needed (I have some suspicious, but they break if multiple threads use the same io_context). But I see that in the case of echo_op they can potentially be different and so the executor_work_guard is there, fine.
Since this is not a "primitive" async operation (that calls directly into the OS and manages operation suspension), we don't need to maintain an executor_work_guard for the CompletionHandler's executor.
I guess that's my question. Why only "primitive" async operation need the executor_work_guard for the CompletionHandler's executor?
Currently, the IoExecutor is always io_context::executor_type, however there is a proposal (https://wg21.link/p1322r0) to enable users to provide custom Executors to IoObjects.
I guess the example is fine no matter if the proposal is accepted or
not since it uses
`decltype(std::declval
Don't really know why if they are equal the executor_work_guard is not needed (I have some suspicious, but they break if multiple threads use the same io_context). But I see that in the case of echo_op they can potentially be different and so the executor_work_guard is there, fine. The work guard is not necessary in such a case because the operation at the bottom maintains a work guard for the handler's executor (which also happens to be the same as the IO object's one). Note that nobody takes advantage of this because it's in general not possible to determine this at compile time and having 2 instantiations of the template outweighs any gains from not maintaining the work count for 1 redundant work item.
I guess that's my question. Why only "primitive" async operation need the executor_work_guard for the CompletionHandler's executor? Composed operations are allowed to maintain additional work guards, but it's not necessary. The work counting mechanism indicates to the executor that "there is an operation pending that you can't see, trust me it will complete sooner or later". The operation at the bottom is responsible for suspending the composed operation and calling into the "OS" (or an abstraction layer on top of it), therefore it's the one that has knowledge about pending work.
I guess the example is fine no matter if the proposal is accepted or not since it uses `decltype(std::declval
().get_executor()`, right? Correct.
On Sat, 24 Nov 2018 at 23:46, Damian Jarek
Don't really know why if they are equal the executor_work_guard is not needed (I have some suspicious, but they break if multiple threads use the same io_context). But I see that in the case of echo_op they can potentially be different and so the executor_work_guard is there, fine. The work guard is not necessary in such a case because the operation at the bottom maintains a work guard for the handler's executor (which also happens to be the same as the IO object's one). Note that nobody takes advantage of this because it's in general not possible to determine this at compile time and having 2 instantiations of the template outweighs any gains from not maintaining the work count for 1 redundant work item.
I guess that's my question. Why only "primitive" async operation need the executor_work_guard for the CompletionHandler's executor? Composed operations are allowed to maintain additional work guards, but it's not necessary. The work counting mechanism indicates to the executor that "there is an operation pending that you can't see, trust me it will complete sooner or later". The operation at the bottom is responsible for suspending the composed operation and calling into the "OS" (or an abstraction layer on top of it), therefore it's the one that has knowledge about pending work.
I guess the example is fine no matter if the proposal is accepted or not since it uses `decltype(std::declval
().get_executor()`, right? Correct.
I actually think I understood everything you said, and I do agree with all of it. But I still have a bad feeling of not completing understanding how work guards work as they work. I guess my main issue is that I see _one_ single io_context.run() which needs to know it should not return, but there are _two_ Executors, both with the need for work guards. When trying to find an example I end up seeing that any obvious CompletionHandler Executor's on_work_started() simply ends up delegating the call to the IO object's one (for example, strand: https://github.com/boostorg/asio/blob/5ac54042c99d4f1595d4041b00b9b28752eda1...) or does nothing (use_future, https://github.com/boostorg/asio/blob/5ac54042c99d4f1595d4041b00b9b28752eda1...). So I'm struggling to see why there would ever, in practice, be the need for two work guards for one single asynchronous operation, even if the CompletionHandler Executor is different to the IO object's one. I guess potentially the CompletionHandler Executor could do "something" with that information, but... what? You said "the operation at the bottom maintains a work guard for the handler's executor". But isn't Networking TS 13.2.7.10 saying the operation at the bottom maintains a work guard for both the handler's executor and the IO object's one? If so, echo_op doesn't need any work_guard at all, does it? You said "Composed operations are allowed to maintain additional work guards, but it's not necessary.". I would agree to this, and in the specific case of echo_op is not necessary, is it? If it's not necessary, why does the echo_op example use one? If it's going to use one, shouldn't it use two to be coherent with 13.2.7.10? The comments in lines 91-94 of the example seem to reference Networking TS to say that only the IO object's one is necessary (and that it *is* necessary).
Here's an example of what might happen if a composed operation doesn't
maintain work guards properly:
https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Without the work_guard the coroutine never completes. If you add the
work_guard, everything works correctly.
In general, the Executors in the TS and ASIO don't do anything fancy with
the information about pending work, because they have wide contracts. In
theory, the TS allows Executors to have much narrower contracts.
In principle, the last call to `on_work_finished()` is allowed to delete
all shared state related to an operation, thus hijacking the work counting
mechanism to replace the need for `shared_ptr`.
Note that I'm describing the behavior of the current reference
implementation of the TS (ASIO) and I'm currently trying to figure out why
the behavior differs. Analyzing standardeese is hard :).
On Mon, Dec 3, 2018 at 7:06 PM Cristian Morales Vega
On Sat, 24 Nov 2018 at 23:46, Damian Jarek
wrote: Don't really know why if they are equal the executor_work_guard is not needed (I have some suspicious, but they break if multiple threads use the same io_context). But I see that in the case of echo_op they can potentially be different and so the executor_work_guard is there, fine. The work guard is not necessary in such a case because the operation at
the bottom maintains a work guard for the handler's executor (which also happens to be the same as the IO object's one). Note that nobody takes advantage of this because it's in general not possible to determine this at compile time and having 2 instantiations of the template outweighs any gains from not maintaining the work count for 1 redundant work item.
I guess that's my question. Why only "primitive" async operation need the executor_work_guard for the CompletionHandler's executor? Composed operations are allowed to maintain additional work guards, but
it's not necessary. The work counting mechanism indicates to the executor that "there is an operation pending that you can't see, trust me it will complete sooner or later". The operation at the bottom is responsible for suspending the composed operation and calling into the "OS" (or an abstraction layer on top of it), therefore it's the one that has knowledge about pending work.
I guess the example is fine no matter if the proposal is accepted or
not since it uses `decltype(std::declval
().get_executor()`, right? Correct.
I actually think I understood everything you said, and I do agree with all of it. But I still have a bad feeling of not completing understanding how work guards work as they work.
I guess my main issue is that I see _one_ single io_context.run() which needs to know it should not return, but there are _two_ Executors, both with the need for work guards.
When trying to find an example I end up seeing that any obvious CompletionHandler Executor's on_work_started() simply ends up delegating the call to the IO object's one (for example, strand:
https://github.com/boostorg/asio/blob/5ac54042c99d4f1595d4041b00b9b28752eda1... ) or does nothing (use_future,
https://github.com/boostorg/asio/blob/5ac54042c99d4f1595d4041b00b9b28752eda1... ). So I'm struggling to see why there would ever, in practice, be the need for two work guards for one single asynchronous operation, even if the CompletionHandler Executor is different to the IO object's one. I guess potentially the CompletionHandler Executor could do "something" with that information, but... what?
You said "the operation at the bottom maintains a work guard for the handler's executor". But isn't Networking TS 13.2.7.10 saying the operation at the bottom maintains a work guard for both the handler's executor and the IO object's one? If so, echo_op doesn't need any work_guard at all, does it?
You said "Composed operations are allowed to maintain additional work guards, but it's not necessary.". I would agree to this, and in the specific case of echo_op is not necessary, is it? If it's not necessary, why does the echo_op example use one? If it's going to use one, shouldn't it use two to be coherent with 13.2.7.10? The comments in lines 91-94 of the example seem to reference Networking TS to say that only the IO object's one is necessary (and that it *is* necessary).
On 7/12/2018 12:59, Damian Jarek wrote:
Here's an example of what might happen if a composed operation doesn't maintain work guards properly: https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Why use conditional logic and extra storage in operator() when the compiler can do it for you? https://wandbox.org/permlink/HzOlDt8S6txfLNB6
Without the work_guard the coroutine never completes. If you add the work_guard, everything works correctly.
I can see it happening in your example, but I still don't really grok why this occurs. Isn't the point of composed operations to ensure that they use the same executor for all handlers? So it's the same executor as the underlying timer. When it's in a wait operation, the timer should be taking care of it. And while in the direct call context of operator() then the executor itself should know there is work in progress. So the only time where the work_guard should be having any effect is either if async_foo itself yields (which it does, but only after creating the op and making a call to async_wait, so that should keep it alive) or if the call to handler_ yields somewhere else (which it doesn't). (handler_() might internally post and yield rather than executing synchronously, especially cross-context, but in that case its executor should know that it's doing something.) So what am I missing? (I guess one of the things that I might be struggling with is that in Ye Olde Asio, as long as you always had an async_* in flight at all times then you never needed any io_service::work. Usually that was easy because you typically have a listen or read in flight.)
On Fri, 7 Dec 2018 at 02:00, Gavin Lambert via Boost-users
On 7/12/2018 12:59, Damian Jarek wrote:
Here's an example of what might happen if a composed operation doesn't maintain work guards properly: https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Nice example!
Why use conditional logic and extra storage in operator() when the compiler can do it for you? https://wandbox.org/permlink/HzOlDt8S6txfLNB6
Without the work_guard the coroutine never completes. If you add the work_guard, everything works correctly.
I can see it happening in your example, but I still don't really grok why this occurs.
Isn't the point of composed operations to ensure that they use the same executor for all handlers? So it's the same executor as the underlying timer. When it's in a wait operation, the timer should be taking care of it. And while in the direct call context of operator() then the executor itself should know there is work in progress.
So the only time where the work_guard should be having any effect is either if async_foo itself yields (which it does, but only after creating the op and making a call to async_wait, so that should keep it alive) or if the call to handler_ yields somewhere else (which it doesn't).
(handler_() might internally post and yield rather than executing synchronously, especially cross-context, but in that case its executor should know that it's doing something.)
So what am I missing?
(I guess one of the things that I might be struggling with is that in Ye Olde Asio, as long as you always had an async_* in flight at all times then you never needed any io_service::work. Usually that was easy because you typically have a listen or read in flight.)
If you look at https://www.boost.org/doc/libs/develop/doc/html/boost_asio/reference/asynchr... under "Outstanding work" it says it will keep the work guards "Until the asynchronous operation has completed". That raises the question of what "completed" means. It's explained at the top, it says "The lifecycle of an asynchronous operation..." — Phase 2: The asynchronous operation is now completed. — Event 3: The completion handler is called with the result of the asynchronous operation. So, it looks like timer_ does keep a work guard for ctx1. But it destroys it before we can call handler_(ec), before calling the completion handler. I though it was not "completed" until after the completion handler was called, but was wrong and that was what confused me. In that example: - ctx2 calls timer_.async_wait(std::move(*this)); - the operation keeps work guards for ctx1 and ctx2 (not that the one for ctx1 matters here, ctx1.run() has not yet been called) - ctx1.run_for(std::chrono::seconds{5}) is called, the timer_ work guards get destroyed and the timer_ completion handler ends up in ctx2 queue - ctx1.run_for returns because it has no work - ctx2 runs the completion handler, which ends up calling timer_.async_wait again - t1 is long gone and that async_wait will never complete In https://wandbox.org/permlink/eNBzNmM2FbL4MHlE "T1 complete" is printed before "Completion Handler called". When there is only one thread involved "timer_ completion handler ends up in ctx2 queue" is not true, the handler gets executed directly without going through the queue. In those cases I guess the work guard is likely to stay there until the completion handler has executed. But having to go through the queue means the timer_ work guards disappear too soon and you can't rely on them. But... if I have got it right, then the echo_op example from Beast is wrong, isn't it? It's doing the right thing keeping a work guard for the AsyncStream associated executor. But it should *also* keep a work guard for the handler associated executor. Damian said: "The work guard is not necessary in such a case because the operation at the bottom maintains a work guard for the handler's executor", but that doesn't seem to be true... for long enough.
You're correct, the work_guards are created, but they may not live long
enough.
As for the echo example in Beast: looks to me like there is a work guard
for the I/O executor in state of the echo_op, so it seems to be correct.
On Fri, Dec 7, 2018 at 11:10 AM Cristian Morales Vega
On Fri, 7 Dec 2018 at 02:00, Gavin Lambert via Boost-users
wrote: On 7/12/2018 12:59, Damian Jarek wrote:
Here's an example of what might happen if a composed operation doesn't maintain work guards properly: https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Nice example!
Why use conditional logic and extra storage in operator() when the compiler can do it for you? https://wandbox.org/permlink/HzOlDt8S6txfLNB6
Without the work_guard the coroutine never completes. If you add the work_guard, everything works correctly.
I can see it happening in your example, but I still don't really grok why this occurs.
Isn't the point of composed operations to ensure that they use the same executor for all handlers? So it's the same executor as the underlying timer. When it's in a wait operation, the timer should be taking care of it. And while in the direct call context of operator() then the executor itself should know there is work in progress.
So the only time where the work_guard should be having any effect is either if async_foo itself yields (which it does, but only after creating the op and making a call to async_wait, so that should keep it alive) or if the call to handler_ yields somewhere else (which it doesn't).
(handler_() might internally post and yield rather than executing synchronously, especially cross-context, but in that case its executor should know that it's doing something.)
So what am I missing?
(I guess one of the things that I might be struggling with is that in Ye Olde Asio, as long as you always had an async_* in flight at all times then you never needed any io_service::work. Usually that was easy because you typically have a listen or read in flight.)
If you look at https://www.boost.org/doc/libs/develop/doc/html/boost_asio/reference/asynchr... under "Outstanding work" it says it will keep the work guards "Until the asynchronous operation has completed". That raises the question of what "completed" means. It's explained at the top, it says "The lifecycle of an asynchronous operation..."
— Phase 2: The asynchronous operation is now completed. — Event 3: The completion handler is called with the result of the asynchronous operation.
So, it looks like timer_ does keep a work guard for ctx1. But it destroys it before we can call handler_(ec), before calling the completion handler. I though it was not "completed" until after the completion handler was called, but was wrong and that was what confused me.
In that example: - ctx2 calls timer_.async_wait(std::move(*this)); - the operation keeps work guards for ctx1 and ctx2 (not that the one for ctx1 matters here, ctx1.run() has not yet been called) - ctx1.run_for(std::chrono::seconds{5}) is called, the timer_ work guards get destroyed and the timer_ completion handler ends up in ctx2 queue - ctx1.run_for returns because it has no work - ctx2 runs the completion handler, which ends up calling timer_.async_wait again - t1 is long gone and that async_wait will never complete
In https://wandbox.org/permlink/eNBzNmM2FbL4MHlE "T1 complete" is printed before "Completion Handler called".
When there is only one thread involved "timer_ completion handler ends up in ctx2 queue" is not true, the handler gets executed directly without going through the queue. In those cases I guess the work guard is likely to stay there until the completion handler has executed. But having to go through the queue means the timer_ work guards disappear too soon and you can't rely on them.
But... if I have got it right, then the echo_op example from Beast is wrong, isn't it? It's doing the right thing keeping a work guard for the AsyncStream associated executor. But it should *also* keep a work guard for the handler associated executor. Damian said: "The work guard is not necessary in such a case because the operation at the bottom maintains a work guard for the handler's executor", but that doesn't seem to be true... for long enough.
On Sat, 15 Dec 2018 at 21:21, Damian Jarek
As for the echo example in Beast: looks to me like there is a work guard for the I/O executor in state of the echo_op, so it seems to be correct.
What I'm concerned about now is the lack of a work guard for the Executor associated with the CompletionHandler. I have modified your example here https://wandbox.org/permlink/IwmegLNlAKPs9tDP . It does work without the second work guard, but not sure why. I'm concerned about the work count reaching zero in the fourth output line, before the first "Completion Handler called". https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio/reference/io_conte... says: "Once the count of unfinished work reaches zero, the io_context is stopped and the run() and run_one() functions may exit." So ASIO would have been free to stop ctx2 there, right?
A currently running handler is considered work, so you don't need the second guard, because before the handler is dispatched, it's the timer's responsibility to maintain the guard for ctx2. After it calls `ctx.get_executor().dispatch()`, it will reset the guard, but the context will not stop until the handler is executed and returns. On Mon, Dec 17, 2018 at 11:49 PM Cristian Morales Vega < cristian@samknows.com> wrote:
On Sat, 15 Dec 2018 at 21:21, Damian Jarek
wrote: As for the echo example in Beast: looks to me like there is a work guard for the I/O executor in state of the echo_op, so it seems to be correct.
What I'm concerned about now is the lack of a work guard for the Executor associated with the CompletionHandler.
I have modified your example here https://wandbox.org/permlink/IwmegLNlAKPs9tDP . It does work without the second work guard, but not sure why. I'm concerned about the work count reaching zero in the fourth output line, before the first "Completion Handler called".
https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio/reference/io_conte... says: "Once the count of unfinished work reaches zero, the io_context is stopped and the run() and run_one() functions may exit." So ASIO would have been free to stop ctx2 there, right?
On Mon, 17 Dec 2018 at 23:00, Damian Jarek
A currently running handler is considered work, so you don't need the second guard, because before the handler is dispatched, it's the timer's responsibility to maintain the guard for ctx2. After it calls `ctx.get_executor().dispatch()`, it will reset the guard, but the context will not stop until the handler is executed and returns.
OK. And the problem with my example is that my_executor::on_work_started() doesn't really count all the outstanding work since things like https://github.com/boostorg/asio/blob/a6008b6427ddfc3222f83c53030c34c802c53b... are invisible to it. Makes sense.
On Thu, 6 Dec 2018 at 23:59, Damian Jarek
Here's an example of what might happen if a composed operation doesn't maintain work guards properly: https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Without the work_guard the coroutine never completes. If you add the work_guard, everything works correctly.
If I were to do something stupid like https://wandbox.org/permlink/sIpX20mQdaHgjESz - Hide executor_type making it private - Use the handler executor type for the work guard What would be the best way to unit test async_foo() to detect the problems? I guess I could derive from io_context to get the steady_timer to use an intermediate executor that would forward everything to the io_context::get_executor but keeping record of the work count, and bind a similar fake executor to check the dispatch calls. But there may be a more clever/simpler way?
You can either check in the completion handler that
`ex.running_in_this_thread() == true` or wrap the inner executor and use a
thread-local variable to determine whether a current thread is running in
the executor's context.
On Fri, Jan 25, 2019 at 4:50 PM Cristian Morales Vega
Here's an example of what might happen if a composed operation doesn't
On Thu, 6 Dec 2018 at 23:59, Damian Jarek
wrote: maintain work guards properly: https://wandbox.org/permlink/aqsGDNJWTmFd7PdC
Without the work_guard the coroutine never completes. If you add the work_guard, everything works correctly.
If I were to do something stupid like https://wandbox.org/permlink/sIpX20mQdaHgjESz - Hide executor_type making it private - Use the handler executor type for the work guard
What would be the best way to unit test async_foo() to detect the problems?
I guess I could derive from io_context to get the steady_timer to use an intermediate executor that would forward everything to the io_context::get_executor but keeping record of the work count, and bind a similar fake executor to check the dispatch calls. But there may be a more clever/simpler way?
participants (3)
-
Cristian Morales Vega
-
Damian Jarek
-
Gavin Lambert