123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- [/
- 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]
|