(Recorded on 2022-05-05)
In this lecture we continue our journal to solve the critical section problem.
We discuss the critical section problem and a couple of approaches for solving it.
The critical section problem is when n processes all competing to use some shared data, each process has a code segment, called critical section, in which the shared data is accessed. The key of the problem is to ensure that when one process is executing in its critical section, no other process is allowed to execute in its critical section.
There are three conditions for the solutions of the critical section problem:
Mutual Exclusion - If process Pi is executing in its critical section, then no other processes can be executing in their critical sections
Progress - If no process is executing in its critical section and there exist some processes that wish to enter their critical section, then the selection of the processes that will enter the critical section next cannot be postponed indefinitely
Bounded Waiting - A bound must exist on the number of times that other processes are allowed to enter their critical sections after a process has made a request to enter its critical section and before that request is granted
We talk about several technologies that we can use to address the critical section problem.
std::mutex
¶The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.
We use the our previous example which approximates the value of \(\pi\) to demonstrate our solution using std::mutex
.
If the control returns without explicit release the lock, we have a deadlock in this case. To avoid this, std::lock
and std::lock_guard
provide us mechanisms to avoid deadlock. Moreover, std::lock
can be used to lock multiple mutexes at the same time.
std::async
¶Besides doing our computation tasks in synchronization, we can also perform our tasks in an asynchronous fashion.
We can launch a task with std::async
, pass the arguments to a task, use std::future
to wait on the values from a task. Once the value is computed, we can uses get()
method of std::future
to get the computation results.
By computing in asynchronous way, we can avoid critical section problem overall by eliminate the race condition on a global shared variable.
A good practice of using std::async
is to alway specify the async launch strategies to avoid unexpected behaviors.
std::atomic
¶Finally, we introduce std::atomic
in the C++ atomics library. Each instantiation and full specialization of the std::atomic
template defines an atomic type. After you declare a std::atomic
data type, you can have atomic operation over that variable without using lock.