diff --git a/doc/qbk/00_main.qbk b/doc/qbk/00_main.qbk index 81e7a5f..a65351f 100644 --- a/doc/qbk/00_main.qbk +++ b/doc/qbk/00_main.qbk @@ -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] diff --git a/doc/qbk/07_asio_compliance.qbk b/doc/qbk/07_asio_compliance.qbk new file mode 100644 index 0000000..62e9156 --- /dev/null +++ b/doc/qbk/07_asio_compliance.qbk @@ -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] diff --git a/doc/qbk/08_executors.qbk b/doc/qbk/08_executors.qbk new file mode 100644 index 0000000..1d6c8a4 --- /dev/null +++ b/doc/qbk/08_executors.qbk @@ -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 client(strand); + + client.brokers("", 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( + "", "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 client(ioc.get_executor()); + + auto strand = boost::asio::make_strand(ioc.get_executor()); + client.brokers("", 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( + "", "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 client(ioc.get_executor()); + + auto strand = boost::asio::make_strand(ioc.get_executor()); + client.brokers("", 1883) + .async_run(boost::asio::detached); + + client.async_publish( + "", "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]