PrevUpHomeNext

Using the mqtt_client in a Multithreaded Environment

This chapter provides information about thread safety of the mqtt_client and other Boost.Asio-compliant objects and provides examples of how to write thread-safe code in multithreaded environments.

A common misconception exists regarding the "thread safety" of ASIO-compliant asynchronous objects, specifically around the belief that initialising such an object with a boost::asio::io_context::strand [7] executor allows its asynchronous functions (async_xxx) to be freely called from any executor or thread. That is not correct. Those async_xxx functions themselves must be called from within the same executor.

Every async_xxx function in every ASIO-compliant object begins by executing some initiation code before typically proceeding to call an intermediate lower-level ASIO async_xxx function, with an adapted handler serving as the callback. It is worth noting that the thread safety of this initiation code, which is called directly by the boost::asio::async_initiate function and executed by the same executor that called the async_xxx function, is not guaranteed and depends on the implementation itself. This uncertainty around thread safety is what the notation "Thread Safety: Shared objects: Unsafe" means, which appears in the documentation for any ASIO-compliant object.

Consequently, similar to the other ASIO-compliant objects, the mqtt_client object is not thread-safe. Invoking its member functions concurrently from separate threads will result in a race condition.

This design choice is intentional and offloads the responsibility of managing concurrency to the user. Given that many applications using Boost.Asio often operate in a single-threaded environment for better performance, ASIO-compliant objects do not want to pay the cost of the overhead associated with mutexes and other synchronization mechanisms. Instead, it encourages developers to implement their own concurrency management strategies, tailoring them to the specific needs of their applications.

Before delving into thread-safe programming, it is essential to understand the distinction between executors and threads. Executors are not threads but mechanisms for scheduling how and when work gets done. An executor can distribute tasks across multiple threads, and a single thread can execute tasks from multiple executors. Thus, when several threads invoke boost::asio::io_context::run() on the same boost::asio::io_context, the underlying executor of that io_context has the flexibility to assign tasks to any of those threads.

A boost::asio::io_context::strand executor is particularly important in maintaining thread safety and managing multithreading. As outlined earlier, this type of executor guarantees that tasks assigned to it are executed in a serialised manner, preventing concurrent execution. It is important to note that this serialisation does not mean that a single thread handles all tasks within a strand. If the io_context associated with a strand operates across multiple threads, these threads can independently undertake tasks within the strand. However, these tasks are executed in a non-concurrent fashion as guaranteed by the strand. Refer to Strands: Use Threads Without Explicit Locking for more details.

As mentioned previously, it is the user's responsibility to ensure that none of the mqtt_client's member functions are called concurrently from separate threads. To achieve thread safety in a multithreaded environment, all the mqtt_client's member functions must be executed within the same implicit [8] or explicit strand.

Specifically, use boost::asio::post or boost::asio::dispatch to delegate a function call to a strand, or boost::asio::co_spawn to spawn the coroutine into the strand. For asynchronous functions, this will ensure that the initiation code is executed within the strand in a thread-safe manner. mqtt_client's executor must be that same strand. This will guarantee that the entire sequence of operations - the initiation code and any intermediate operation - is carried out within the strand, thereby ensuring thread safety.

[Important] Important

To conclude, to achieve thread safety, all the member functions of the mqtt_client must be executed in the same strand. This strand must be given in the mqtt_client constructor.

The examples below demonstrate how to publish a "Hello World" Application Message in a multithreaded setting using callbacks (post/dispatch) and coroutines (co_spawn):



[7] An executor that provides serialised handler execution.

[8] Only one thread is calling boost::asio::io_context::run().


PrevUpHomeNext