Add a chapter on asio compliance - executors

Summary: related to T12804

Reviewers: ivica

Reviewed By: ivica

Subscribers: miljen, iljazovic

Differential Revision: https://repo.mireo.local/D28496
This commit is contained in:
Korina Šimičević
2024-03-18 13:16:12 +01:00
parent 8dfbdf2d38
commit 0d6bbbc424
3 changed files with 156 additions and 0 deletions

View File

@ -116,6 +116,7 @@
[include 04_maintaining_a_stable_connection.qbk]
[include 05_optimising_communication.qbk]
[include 06_disconnecting_the_client.qbk]
[include 07_asio_compliance.qbk]
[include 10_examples.qbk]

View File

@ -0,0 +1,14 @@
[section:asio_compliance Compliance with Boost.Asio]
Every asynchronous operation in __Asio__ has associated characteristics that specify their behaviour.
* An *allocator* determines how the asynchronous operations allocate memory resources.
* A *cancellation slot* determines how the asynchronous operations support cancellation.
* An *executor* determines the queuing and execution strategy for completion handlers.
This section expands further into the roles of allocators, cancellation slots, and [link async_mqtt5.asio_compliance.executors Executors],
highlighting their integration and usage within the __Client__.
[include 08_executors.qbk]
[endsect] [/asio_compliance]

141
doc/qbk/08_executors.qbk Normal file
View File

@ -0,0 +1,141 @@
[section:executors Executors]
Every asynchronous operation has an associated executor that determines how the completion handlers are queued and run.
Asynchronous operations use the associated executor to track the existence of the work of asynchronous tasks,
schedule the completion handlers for execution,
and prevent re-entrant execution of the completion handlers to avoid recursion and potential stack overflow issues.
Every asynchronous operation within the __Client__ is defined as a composed operation.
This implies that each `async_xxx` operation is a sequence consisting of an initiating function,
a series of intermediate asynchronous operations, and a final completion handler.
Upon creating an instance of the __Client__, it is necessary to provide an executor or an __ExecutionContext__.
The specified executor (or __ExecutionContext__'s executor) will become the default executor associated with the __Client__
and will be used for the execution of all the intermediate operations and a final completion handler for all asynchronous operations
that have not bound an executor.
If an executor is bound to an asynchronous operation, that executor will be used instead.
In this context, the [refmem mqtt_client async_run] operation is particularly important.
It starts the __Client__, which initiates a series of internal asynchronous operations, all of which need an executor.
If the [refmem mqtt_client async_run] is called with a completion handler that has an associated executor,
then all the internal asynchronous operations will also be associated with the same executor.
Otherwise, the default executor from the __Client__'s construction will be used instead.
[note
If the [refmem mqtt_client async_run]'s completion handler has an associated executor,
*the associated executor will become the new default associated executor* instead of the executor provided in the constructor.
]
[important
The same executor *must* execute [refmem mqtt_client async_run] and all the subsequent async_xxx operations.
]
The following examples will demonstrate the previously described interactions.
[heading Example 1: using the constructor's executor as the default associated executor]
In this code snippet, the __Client__ is constructed with a strand
without explicitly associating an executor with the completion handler of [refmem mqtt_client async_run].
Consequently, the __Client__ adopts the strand as its new default executor,
which is used to execute the [refmem mqtt_client async_publish] operation.
```
int main() {
boost::asio::io_context ioc;
// Construct the Client with a strand.
auto strand = boost::asio::make_strand(ioc.get_executor());
async_mqtt5::mqtt_client<boost::asio::ip::tcp::socket> client(strand);
client.brokers("<your-mqtt-broker>", 1883)
.async_run(boost::asio::detached);
// This asynchronous operation will use the default associated executor,
// which is the strand with which the Client is constructed.
client.async_publish<async_mqtt5::qos_e::at_most_once>(
"<topic>", "Hello world!",
async_mqtt5::retain_e::no, async_mqtt5::publish_props {},
[&client, &strand](async_mqtt5::error_code /* ec */) {
assert(strand.running_in_this_thread());
client.cancel();
}
);
ioc.run();
}
```
[heading Example 2: binding an executor to async_run's handler overrides the default associated executor]
In this code snippet, the __Client__ is constructed with __IOC__'s executor
with explicitly associating an executor (strand) with the completion handler of [refmem mqtt_client async_run].
Consequently, the __Client__ adopts the strand as its new default executor,
which is used to execute the [refmem mqtt_client async_publish] operation.
```
int main() {
boost::asio::io_context ioc;
// Create the Client with io_context's executor.
async_mqtt5::mqtt_client<boost::asio::ip::tcp::socket> client(ioc.get_executor());
auto strand = boost::asio::make_strand(ioc.get_executor());
client.brokers("<your-mqtt-broker>", 1883)
// Bind the strand to async_run's completion handler.
// Strand is now the default associated executor.
.async_run(boost::asio::bind_executor(strand, boost::asio::detached));
client.async_publish<async_mqtt5::qos_e::at_most_once>(
"<topic>", "Hello world!",
async_mqtt5::retain_e::no, async_mqtt5::publish_props {},
[&client, &strand](async_mqtt5::error_code /* ec */) {
assert(strand.running_in_this_thread());
client.cancel();
}
);
ioc.run();
}
```
[heading Example 3: binding an executor to a async_xxx call]
In this code snippet, the __Client__ is constructed with __IOC__'s executor
without explicitly associating an executor with the completion handler of [refmem mqtt_client async_run].
The [refmem mqtt_client async_publish] operation bound the strand as the associated executor.
Therefore, all the intermediate operations and the final completion handler will be executed
on the strand.
[warning
This example only serves as a demonstration and should *not* be used.
It is *not* recommended that [refmem mqtt_client async_run] and other async_xxx operations execute on different executors!
]
```
int main() {
boost::asio::io_context ioc;
async_mqtt5::mqtt_client<boost::asio::ip::tcp::socket> client(ioc.get_executor());
auto strand = boost::asio::make_strand(ioc.get_executor());
client.brokers("<your-mqtt-broker>", 1883)
.async_run(boost::asio::detached);
client.async_publish<async_mqtt5::qos_e::at_most_once>(
"<topic>", "Hello world!",
async_mqtt5::retain_e::no, async_mqtt5::publish_props {},
boost::asio::bind_executor(
strand,
[&client, &strand](async_mqtt5::error_code /* ec */) {
assert(strand.running_in_this_thread());
client.cancel();
}
)
);
ioc.run();
}
```
[endsect] [/executors]