[Running multiple threads] – [Safe access to shared data] – [Wait and signal – std::condition_variable]
Newer features:
[Semaphore]: limit access to a shared resource
[std::latch]: wait until a counter becomes zero
[std::jthread]: automatically joining, possibility for stop
[coroutines]: functions with state, can suspend and resume their execution
[std::osyncstream]: synchronized writing to std::out
Introduction
At the very beginning the most important note about multi threading says:
Do not use multi threading if you do not have a very good reason for it!
Possible reasons where multithreading may be recommended:
- staying reactive
Sometimes an application has to perform long enduring actions (e.g. calculating complex algorithms, searching within big data, writing to external devices). For single threaded applications the user would have to wait for the end of the operation. The user interface would be no longer reactive and even a cancelling of the action would not be possible. - using blocking communication methods – listener thread
When waiting for input data from some communication channel (e.g. CAN bus) it is common practice to use a listener thread that performs a blocking wait for arrival of next data. - synchronizing with threads
If you already have a multi threaded application then additional threads may be introduced as a means of synchronization (e.g. a message queue thread serializes all actions and replaces usage of mutexes) - improving reaction time
Instead of performing your work within the notifying thread directly you may post to a worker thread. Then the calling thread can immediately continue with its work e.g. perform time critical actions. - using modern HW
If you really have long enduring complex algorithms and if you are running your program on a HW with multiple processors you may take the effort for splitting your algorithm into several parts, each running on its own processsor.
In most cases the big effort and risk of changing approved algorithms is not worth the small speed improvements for some situations.
Typical statements – wrong in most cases!
- object A runs within a specific thread
typically an object offers an interface or some public methods to be called by its clients. A client can use any thread he has access to for calling A’s method. Object A simply has no influence on the choice of its clients. As a consequence requests may arrive on any thread. - object B has a thread or some thread belongs to an object
Object B may create a thread. But in the moment when object B calls some other part of the SW these part has full access to B’s thread and can do any thing with it (e.g. block it for an indefinite time length)
How to deal with multi threading
- running multiple threads is pretty easy, but often the problem of properly synchronizing access to shared data causes greater headaches.
- For an introductory example of missing synchronization see Example 1: Running threads with unsynchronized data access
- For an introductory example with added synchronization see Example 2: Adding synchronization for data access
- To avoid problems like deadlocks special care has to be taken. You may need
- contracts / agreements which apply when calling interface functions of other components
- rules how to behave within locked sections
The following sections will give more informations about handling data and threads correctly.
Running multiple threads
This section describes how you can execute your tasks within multiple threads.
Safe access to shared data
This section describes how you can have safe access to the same data from different threads.
Wait and signal – std::condition_variable
This section describes how you can wait until some work has been done within some other thread.