C++ Reactive Programming
上QQ阅读APP看书,第一时间看更新

Sharing data between threads

We have seen how to start a thread and different methods of managing them. Now, let's discuss how to share data between threads. One key feature of concurrency is its ability to share data between the threads in action. First, let's see what the problems associated with threads accessing common (shared) data are.

There won't be a problem if the data shared between threads is immutable (read-only), because the data read by one thread is unaffected by whether the other threads are reading the same data or not. The moment threads start modifying shared data is when problems begin to emerge.

For example, if the threads are accessing a common data structure, the invariants associated with the data structure are broken if an update is happening. In this case, the number of elements is stored in the data structure, which usually requires the modification of more than one value. Consider the delete operation of a self-balancing tree or a doubly linked list. If you don't do anything special to ensure otherwise, if one thread is reading the data structure, while another is removing a node, it is quite possible for the reading thread to see the data structure with a partially removed node, so the invariant is broken. This might end up corrupting the data structure permanently and could lead to the program crashing.

An invariant is a set of assertions that must always be true during the execution of a program or lifetime of an object. Placing proper assertion within the code to see whether invariants have been violated will result in robust code. This is a great way to document software as well as a good mechanism to prevent regression bugs. More can be read about this in the following Wikipedia article: https://en.wikipedia.org/wiki/Invariant_(computer_science).

This often leads to a situation called race condition, which is the most common cause of bugs in concurrent programs. In multithreading, race condition means that the threads race to perform their respective operations. Here, the outcome depends on the relative ordering of the execution of an operation in two or more threads. Usually, the term race condition means a problematic race condition; normal race conditions don't cause any bugs. Problematic race conditions usually occur where the completion of an operation requires modification of two or more bits of data, such as deletion of a node in a tree data structure or a doubly linked list. Because the modification must access separate pieces of data, these must be modified in separate instructions when another thread is trying to access the data structure. This occurs when half of the previous modifications have been completed.

Race conditions are often very hard to find and hard to duplicate because they occur in a very short window of execution. For software that uses concurrency, the major complexity of implementation comes from avoiding problematic race conditions.

There are many ways to deal with problematic race conditions. The common and simplest option is to use synchronization primitives, which are lock-based protection mechanisms. This wraps the data structure by using some locking mechanisms to prevent the access of other threads during its execution. We will discuss the available synchronization primitives and their uses in detail in this chapter.

Another option is to alter the design of your data structure and its invariants so that the modification guarantees the sequential consistency of your code, even across multiple threads. This is a difficult way of writing programs and is commonly referred to as lock-free programming. Lock-free programming and the C++ memory model will be covered in Chapter 4Asynchronous and Lock-Free Programming in C++.

Then, there are other mechanisms such as handling the updates to a data structure as a transaction, as updates to databases are done within transactions. Currently, this topic is not in the scope of this book, and therefore it won't be covered.

Now, let's consider the most basic mechanism in C++ standard for protecting shared data, which is the mutex.