Lecture 12: Tasks, async, C++ Concurrency

../_images/L12-sp22-title.png Lecture 12 slides Lecture 12 panopto Podcast

(Recorded on 2022-05-05)

In this lecture we continue our journal to solve the critical section problem.

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.

../_images/async.png

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.