Process owns some memory and contains at least one working thread. Processes communicate with each other through Interproces Communication channels such as: file, pipe, message queue, …
Threads live inside a process and communicate with each other through some shared memory.
std::thread defines a thread object which is accossiated with a launched thread of execution. Threads start with executing a function they were passed during creation. Next we can join() new thread with main thread (main thread waits untill new thread finishes its function) or detach() new thread (main thread doesn’t wait for child threads completion and continues its execution). If thread is detached then it bacomes a deamon thread and will be managed by C++ runtime (it can’t be joined anymore). After calling the detach() the corresponding std::thread object is not associated with the thread of execution. After calling the join() and successfully waiting for the thread’s completion the corresponding std::thread object is not associated with the thread of execution.
We can pass parameters into a function. When we pass function arguments into a thread they are passed by value. They are copied into thread’s internal block from which passed as rvalues into function the thread is going to execute.
To pass parameters by reference we can use std::ref:
We also can pass functors into threads:
std::thread::hardware_concurrency() - returns the number of threads supported on the hardware. Having too many threads (oversubscription) can harm performance because of context switching. std::this_thread::get_id() - returns id of the current thread. t1.get_id() - return id of the t1 thread.
After t1.join() t1 thread is finished and its id = 0.
Also we can pass parameters into a thread as rvalue reference, using move semantic (std::move) to reduce unnecessary copies.
Threads can’t be copied but can be modev with std::move.
Race condition and Mutex
Example with race condition over std::cout:
Output is messy because two threads write at the cout simultaneously. To fix this we can use std::mutex to syncrhonize threads. So we need to “bind” cout with mutex.
This will work but it is still not safe - if an exception occures during cout (after the mutex was locked) the mutex will remain locked and no one will be able to use shared_cout function. So it’s not recomended practise to use mutex directly. Instead we can use RAII semantic - create a mutex wrapper on stack which will take and lock a mutex when created and unlock when we go out of its scope (std::lock_guard).
Using this synchronization method makes std::cout thread safe when using through shared_cout().
Avoid data race:
Use mutex to sinchronize data access
Do not leak a reference or pointer as it allows to modify the shared data without protection
Design thread-safe interfaces (for example, stack has .pop() and .top() methods, suppose two threads look at the top of the stack with .top() method and decide to remove it, so the first thread calls .pop() and the second thread will pop different element)
Deadlock
When we use more than one mutex to protect shared data deadlock can happen. For example, one thread locks mu1 and mu2, another thread locks mu2 and mu1. Then it is possible that mu1 will be locked by thread 1 and mu2 by thread 2. Then both threads will wait to lock mu1 and mu2.
To avoid deadlock:
Try to use one mutex
Use std::lock(mu1, mu2, …) function to safely lock more than one mutex (it has some anti deadlock mechanizm)
Lock mutexes in the same order (mu1 -> mu2)
Avoid locking a mutex and then calling some user defined function (we don’t know but it may lock some other mutexes)
Unique_lock
Gives more flexebilities than lock_guard. With unique_lock we can lock, unlock mutex, deffer locking. It also can be moved (with std::move) in contrust to std::lock_guard.
unique_lock is a little havier then lock_guard.
Once flag
If we need to perform a function only once an in thread safe manner we can use std::once_flag flag and std::call_once function. In the example below the shared_cout_2 will be called only once.
Conditional variables
Suppose we have two worker threads, one of which supplies some data and the other does something with this data. The second thread needs to loop and check if there is data available or sleep. Pooling causes wasted processor’s time and for sleep we don’t know how much time we need to wait. A better solution is to use Conditional variables.
The thread t2 locks the mutex (if it is not locked by the other thread) and starts waiting on the cv. At this moment the mutex is unlocked (so other threads can access the shared resource while this thread waits for a notification). When thread t1 notifies the thread t2 through the cv the t2 locks the mutex and accesses the shared resource. When waiting on a conditional variable thread can spontaneously wake up, to put him back to sleep we use a predicat (lambda in cv.wait()).