58a4b88fa9
- in context of #153
366 lines
14 KiB
Plaintext
366 lines
14 KiB
Plaintext
[/
|
|
Copyright Oliver Kowalke, Nat Goodspeed 2015.
|
|
Distributed under the Boost Software License, Version 1.0.
|
|
(See accompanying file LICENSE_1_0.txt or copy at
|
|
http://www.boost.org/LICENSE_1_0.txt
|
|
]
|
|
|
|
[/ import path is relative to this .qbk file]
|
|
[import ../examples/adapt_callbacks.cpp]
|
|
[import ../examples/adapt_method_calls.cpp]
|
|
|
|
[#callbacks]
|
|
[section:callbacks Integrating Fibers with Asynchronous Callbacks]
|
|
|
|
[section Overview]
|
|
|
|
One of the primary benefits of __boost_fiber__ is the ability to use
|
|
asynchronous operations for efficiency, while at the same time structuring the
|
|
calling code ['as if] the operations were synchronous. Asynchronous operations
|
|
provide completion notification in a variety of ways, but most involve a
|
|
callback function of some kind. This section discusses tactics for interfacing
|
|
__boost_fiber__ with an arbitrary async operation.
|
|
|
|
For purposes of illustration, consider the following hypothetical API:
|
|
|
|
[AsyncAPI]
|
|
|
|
The significant points about each of `init_write()` and `init_read()` are:
|
|
|
|
* The `AsyncAPI` method only initiates the operation. It returns immediately,
|
|
while the requested operation is still pending.
|
|
* The method accepts a callback. When the operation completes, the callback is
|
|
called with relevant parameters (error code, data if applicable).
|
|
|
|
We would like to wrap these asynchronous methods in functions that appear
|
|
synchronous by blocking the calling fiber until the operation completes. This
|
|
lets us use the wrapper function[s] return value to deliver relevant data.
|
|
|
|
[tip [template_link promise] and [template_link future] are your friends here.]
|
|
|
|
[endsect]
|
|
[section Return Errorcode]
|
|
|
|
The `AsyncAPI::init_write()` callback passes only an `errorcode`. If we simply
|
|
want the blocking wrapper to return that `errorcode`, this is an extremely
|
|
straightforward use of [template_link promise] and [template_link future]:
|
|
|
|
[callbacks_write_ec]
|
|
|
|
All we have to do is:
|
|
|
|
# Instantiate a `promise<>` of correct type.
|
|
# Obtain its `future<>`.
|
|
# Arrange for the callback to call [member_link promise..set_value].
|
|
# Block on [member_link future..get].
|
|
|
|
[note This tactic for resuming a pending fiber works even if the callback is
|
|
called on a different thread than the one on which the initiating fiber is
|
|
running. In fact, [@../../examples/adapt_callbacks.cpp the example program[s]]
|
|
dummy `AsyncAPI` implementation illustrates that: it simulates async I/O by
|
|
launching a new thread that sleeps briefly and then calls the relevant
|
|
callback.]
|
|
|
|
[endsect]
|
|
[section Success or Exception]
|
|
|
|
A wrapper more aligned with modern C++ practice would use an exception, rather
|
|
than an `errorcode`, to communicate failure to its caller. This is
|
|
straightforward to code in terms of `write_ec()`:
|
|
|
|
[callbacks_write]
|
|
|
|
The point is that since each fiber has its own stack, you need not repeat
|
|
messy boilerplate: normal encapsulation works.
|
|
|
|
[endsect]
|
|
[section Return Errorcode or Data]
|
|
|
|
Things get a bit more interesting when the async operation[s] callback passes
|
|
multiple data items of interest. One approach would be to use `std::pair<>` to
|
|
capture both:
|
|
|
|
[callbacks_read_ec]
|
|
|
|
Once you bundle the interesting data in `std::pair<>`, the code is effectively
|
|
identical to `write_ec()`. You can call it like this:
|
|
|
|
[callbacks_read_ec_call]
|
|
|
|
[endsect]
|
|
[#Data_or_Exception]
|
|
[section Data or Exception]
|
|
|
|
But a more natural API for a function that obtains data is to return only the
|
|
data on success, throwing an exception on error.
|
|
|
|
As with `write()` above, it[s] certainly possible to code a `read()` wrapper in
|
|
terms of `read_ec()`. But since a given application is unlikely to need both,
|
|
let[s] code `read()` from scratch, leveraging [member_link
|
|
promise..set_exception]:
|
|
|
|
[callbacks_read]
|
|
|
|
[member_link future..get] will do the right thing, either returning
|
|
`std::string` or throwing an exception.
|
|
|
|
[endsect]
|
|
[section Success/Error Virtual Methods]
|
|
|
|
One classic approach to completion notification is to define an abstract base
|
|
class with `success()` and `error()` methods. Code wishing to perform async
|
|
I/O must derive a subclass, override each of these methods and pass the async
|
|
operation a pointer to a subclass instance. The abstract base class might look
|
|
like this:
|
|
|
|
[Response]
|
|
|
|
Now the `AsyncAPI` operation might look more like this:
|
|
|
|
[method_init_read]
|
|
|
|
We can address this by writing a one-size-fits-all `PromiseResponse`:
|
|
|
|
[PromiseResponse]
|
|
|
|
Now we can simply obtain the `future<>` from that `PromiseResponse` and wait
|
|
on its `get()`:
|
|
|
|
[method_read]
|
|
|
|
[/ @path link is relative to (eventual) doc/html/index.html, hence ../..]
|
|
The source code above is found in
|
|
[@../../examples/adapt_callbacks.cpp adapt_callbacks.cpp]
|
|
and
|
|
[@../../examples/adapt_method_calls.cpp adapt_method_calls.cpp].
|
|
|
|
[endsect]
|
|
[#callbacks_asio]
|
|
[section Then There[s] __boost_asio__]
|
|
|
|
[import ../examples/asio/yield.hpp]
|
|
[import ../examples/asio/detail/yield.hpp]
|
|
|
|
Since the simplest form of Boost.Asio asynchronous operation completion token
|
|
is a callback function, we could apply the same tactics for Asio as for our
|
|
hypothetical `AsyncAPI` asynchronous operations.
|
|
|
|
Fortunately we need not. Boost.Asio incorporates a mechanism[footnote This
|
|
mechanism has been proposed as a conventional way to allow the caller of an
|
|
arbitrary async function to specify completion handling:
|
|
[@http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4045.pdf N4045].]
|
|
by which the caller can customize the notification behavior of any async
|
|
operation. Therefore we can construct a ['completion token] which, when passed
|
|
to a __boost_asio__ async operation, requests blocking for the calling fiber.
|
|
|
|
A typical Asio async function might look something like this:[footnote per [@http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4045.pdf N4045]]
|
|
|
|
template < ..., class CompletionToken >
|
|
``['deduced_return_type]``
|
|
async_something( ... , CompletionToken&& token)
|
|
{
|
|
// construct handler_type instance from CompletionToken
|
|
handler_type<CompletionToken, ...>::type ``[*[`handler(token)]]``;
|
|
// construct async_result instance from handler_type
|
|
async_result<decltype(handler)> ``[*[`result(handler)]]``;
|
|
|
|
// ... arrange to call handler on completion ...
|
|
// ... initiate actual I/O operation ...
|
|
|
|
return ``[*[`result.get()]]``;
|
|
}
|
|
|
|
We will engage that mechanism, which is based on specializing Asio[s]
|
|
`handler_type<>` template for the `CompletionToken` type and the signature of
|
|
the specific callback. The remainder of this discussion will refer back to
|
|
`async_something()` as the Asio async function under consideration.
|
|
|
|
The implementation described below uses lower-level facilities than `promise`
|
|
and `future` because the `promise` mechanism interacts badly with
|
|
[@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/io_service/stop.html
|
|
`io_service::stop()`]. It produces `broken_promise` exceptions.
|
|
|
|
`boost::fibers::asio::yield` is a completion token of this kind. `yield` is an
|
|
instance of `yield_t`:
|
|
|
|
[fibers_asio_yield_t]
|
|
|
|
`yield_t` is in fact only a placeholder, a way to trigger Boost.Asio
|
|
customization. It can bind a
|
|
[@http://www.boost.org/doc/libs/release/libs/system/doc/reference.html#Class-error_code
|
|
`boost::system::error_code`]
|
|
for use by the actual handler.
|
|
|
|
`yield` is declared as:
|
|
|
|
[fibers_asio_yield]
|
|
|
|
Asio customization is engaged by specializing
|
|
[@http://www.boost.org/doc/libs/release/doc/html/boost_asio/reference/handler_type.html
|
|
`boost::asio::handler_type<>`]
|
|
for `yield_t`:
|
|
|
|
[asio_handler_type]
|
|
|
|
(There are actually four different specializations in
|
|
[@../../examples/asio/detail/yield.hpp detail/yield.hpp],
|
|
one for each of the four Asio async callback signatures we expect.)
|
|
|
|
The above directs Asio to use `yield_handler` as the actual handler for an
|
|
async operation to which `yield` is passed. There[s] a generic
|
|
`yield_handler<T>` implementation and a `yield_handler<void>` specialization.
|
|
Let[s] start with the `<void>` specialization:
|
|
|
|
[fibers_asio_yield_handler_void]
|
|
|
|
`async_something()`, having consulted the `handler_type<>` traits
|
|
specialization, instantiates a `yield_handler<void>` to be passed as the
|
|
actual callback for the async operation. `yield_handler`[s] constructor accepts
|
|
the `yield_t` instance (the `yield` object passed to the async function) and
|
|
passes it along to `yield_handler_base`:
|
|
|
|
[fibers_asio_yield_handler_base]
|
|
|
|
`yield_handler_base` stores a copy of the `yield_t` instance [mdash] which, as
|
|
shown above, contains only an `error_code*`. It also captures the
|
|
[class_link context]* for the currently-running fiber by calling [member_link
|
|
context..active].
|
|
|
|
You will notice that `yield_handler_base` has one more data member (`ycomp_`)
|
|
that is initialized to `nullptr` by its constructor [mdash] though its
|
|
`operator()()` method relies on `ycomp_` being non-null. More on this in a
|
|
moment.
|
|
|
|
Having constructed the `yield_handler<void>` instance, `async_something()`
|
|
goes on to construct an `async_result` specialized for the
|
|
`handler_type<>::type`: in this case, `async_result<yield_handler<void>>`. It
|
|
passes the `yield_handler<void>` instance to the new `async_result` instance.
|
|
|
|
[fibers_asio_async_result_void]
|
|
|
|
Naturally that leads us straight to `async_result_base`:
|
|
|
|
[fibers_asio_async_result_base]
|
|
|
|
This is how `yield_handler_base::ycomp_` becomes non-null:
|
|
`async_result_base`[s] constructor injects a pointer back to its own
|
|
`yield_completion` member.
|
|
|
|
Recall that the canonical `yield_t` instance `yield` initializes its
|
|
`error_code*` member `ec_` to `nullptr`. If this instance is passed to
|
|
`async_something()` (`ec_` is still `nullptr`), the copy stored in
|
|
`yield_handler_base` will likewise have null `ec_`. `async_result_base`[s]
|
|
constructor sets `yield_handler_base`[s] `yield_t`[s] `ec_` member to point to
|
|
its own `error_code` member.
|
|
|
|
The stage is now set. `async_something()` initiates the actual async
|
|
operation, arranging to call its `yield_handler<void>` instance on completion.
|
|
Let[s] say, for the sake of argument, that the actual async operation[s]
|
|
callback has signature `void(error_code)`.
|
|
|
|
But since it[s] an async operation, control returns at once to
|
|
`async_something()`. `async_something()` calls
|
|
`async_result<yield_handler<void>>::get()`, and will return its return value.
|
|
|
|
`async_result<yield_handler<void>>::get()` inherits
|
|
`async_result_base::get()`.
|
|
|
|
`async_result_base::get()` immediately calls `yield_completion::wait()`.
|
|
|
|
[fibers_asio_yield_completion]
|
|
|
|
Supposing that the pending async operation has not yet completed,
|
|
`yield_completion::completed_` will still be `false`, and `wait()` will call
|
|
[member_link context..suspend] on the currently-running fiber.
|
|
|
|
Other fibers will now have a chance to run.
|
|
|
|
Some time later, the async operation completes. It calls
|
|
`yield_handler<void>::operator()(error_code const&)` with an `error_code` indicating
|
|
either success or failure. We[,]ll consider both cases.
|
|
|
|
`yield_handler<void>` explicitly inherits `operator()(error_code const&)` from
|
|
`yield_handler_base`.
|
|
|
|
`yield_handler_base::operator()(error_code const&)` first sets
|
|
`yield_completion::completed_` `true`. This way, if `async_something()`[s]
|
|
async operation completes immediately [mdash] if
|
|
`yield_handler_base::operator()` is called even before
|
|
`async_result_base::get()` [mdash] the calling fiber will ['not] suspend.
|
|
|
|
The actual `error_code` produced by the async operation is then stored through
|
|
the stored `yield_t::ec_` pointer. If `async_something()`[s] caller used (e.g.)
|
|
`yield[my_ec]` to bind a local `error_code` instance, the actual `error_code`
|
|
value is stored into the caller[s] variable. Otherwise, it is stored into
|
|
`async_result_base::ec_`.
|
|
|
|
If the stored fiber context `yield_handler_base::ctx_` is not already running,
|
|
it is marked as ready to run by passing it to [member_link
|
|
context..schedule]. Control then returns from
|
|
`yield_handler_base::operator()`: the callback is done.
|
|
|
|
In due course, that fiber is resumed. Control returns from [member_link
|
|
context..suspend] to `yield_completion::wait()`, which returns to
|
|
`async_result_base::get()`.
|
|
|
|
* If the original caller passed `yield[my_ec]` to `async_something()` to bind
|
|
a local `error_code` instance, then `yield_handler_base::operator()` stored
|
|
its `error_code` to the caller[s] `my_ec` instance, leaving
|
|
`async_result_base::ec_` initialized to success.
|
|
* If the original caller passed `yield` to `async_something()` without binding
|
|
a local `error_code` variable, then `yield_handler_base::operator()` stored
|
|
its `error_code` into `async_result_base::ec_`. If in fact that `error_code`
|
|
is success, then all is well.
|
|
* Otherwise [mdash] the original caller did not bind a local `error_code` and
|
|
`yield_handler_base::operator()` was called with an `error_code` indicating
|
|
error [mdash] `async_result_base::get()` throws `system_error` with that
|
|
`error_code`.
|
|
|
|
The case in which `async_something()`[s] completion callback has signature
|
|
`void()` is similar. `yield_handler<void>::operator()()` invokes the machinery
|
|
above with a ["success] `error_code`.
|
|
|
|
A completion callback with signature `void(error_code, T)` (that is: in
|
|
addition to `error_code`, callback receives some data item) is handled
|
|
somewhat differently. For this kind of signature, `handler_type<>::type`
|
|
specifies `yield_handler<T>` (for `T` other than `void`).
|
|
|
|
A `yield_handler<T>` reserves a `value_` pointer to a value of type `T`:
|
|
|
|
[fibers_asio_yield_handler_T]
|
|
|
|
This pointer is initialized to `nullptr`.
|
|
|
|
When `async_something()` instantiates `async_result<yield_handler<T>>`:
|
|
|
|
[fibers_asio_async_result_T]
|
|
|
|
this `async_result<>` specialization reserves a member of type `T` to receive
|
|
the passed data item, and sets `yield_handler<T>::value_` to point to its own
|
|
data member.
|
|
|
|
`async_result<yield_handler<T>>` overrides `get()`. The override calls
|
|
`async_result_base::get()`, so the calling fiber suspends as described above.
|
|
|
|
`yield_handler<T>::operator()(error_code, T)` stores its passed `T` value into
|
|
`async_result<yield_handler<T>>::value_`.
|
|
|
|
Then it passes control to `yield_handler_base::operator()(error_code)` to deal
|
|
with waking the original fiber as described above.
|
|
|
|
When `async_result<yield_handler<T>>::get()` resumes, it returns the stored
|
|
`value_` to `async_something()` and ultimately to `async_something()`[s]
|
|
caller.
|
|
|
|
The case of a callback signature `void(T)` is handled by having
|
|
`yield_handler<T>::operator()(T)` engage the `void(error_code, T)` machinery,
|
|
passing a ["success] `error_code`.
|
|
|
|
[/ @path link is relative to (eventual) doc/html/index.html, hence ../..]
|
|
The source code above is found in
|
|
[@../../examples/asio/yield.hpp yield.hpp] and
|
|
[@../../examples/asio/detail/yield.hpp detail/yield.hpp].
|
|
|
|
[endsect]
|
|
[endsect]
|