Exercise 2: Working with the critical_section class (Background)

Figure 1

In the first lesson we saw how not being careful with the data that your program shares can have adverse effects in your application. Revisiting the parallelized version of the primes example:

C++

parallel_for(1,count,1,[&](int i){ if (IsPrime(i)) ++totalPrimes; )};

The problem with this code is that all threads will be accessing and modifying the totalPrimes variable. When you have concurrent access to a shared variable, then a thread could read a value; modify that variable’s value while another thread reads the old variable’s value. At this point, the first thread is going to write the new value to the variable, and later on the second thread will also write the new value to the variable; thus, wiping whatever the first thread wrote. This is a problem commonly known as a data race condition.

Data Race - Storage Conflict

Figure 2

A data race happens the output of a process is not well defined, and it is critically dependent on the timing and sequence of events.

In this particular example, a and b are assigned a specific value. The application then spawns two threads. One thread modifies the value of the variable, while the other thread reads the value from that same variable. Depending on which thread runs first, the value of the variable “b” will be different, and the final value of “x” will also be different between subsequent runs.

There are many ways to avoid data races, one of which is to use a critical section to make sure that only one thread can modify a particular variable. Although this might solve the problem of a data race, critical sections that make other threads wait for long periods of time can hinder the performance of the application. In a worst case scenario, your multi-threaded application could end up performing worse than its serial version.

Motivation

Figure 3

The concurrency runtime’s synchronization primitives have the following goals:

Simple APIs

Unlike their Win32 equivalent, concurrency runtime’s synchronization primitives don’t have C-style initialization and release/destroy types of resource management calls. The exposed interfaces are simple and conform to the C++0x standards and the synchronization objects throw meaningful exceptions on certain illegal operations.

Block in a cooperative manner

The synchronization objects are cooperative in nature, in that they yield to other cooperative tasks in the runtime in addition to preempting.

Concurrency Runtime Critical Section

Figure 4

Critical sections represent a non-reentrant, cooperative mutual exclusion object that uses concurrency runtime’s facilities to enable cooperative scheduling of work when blocked. This class satisfies all Mutex requirements specified in C++0x standards. The concurrency runtime’s critical section provides a C++ façade as compared to its C-styled Win32 equivalent: Windows CRITICAL_SECTION.

Similarity to the Win32 CRITICAL_SECTION:

Can be used only by threads of a single process.

The critical section object can only be owned by one thread at a time.

Differences from Win32 CRITICAL_SECTION:

The concurrency runtime’s critical sections are non-recursive. Exceptions are thrown upon recursive calls.

The concurrency runtime’s critical section object guarantees that threads waiting on a critical section acquire it on a first-come, first-serve basis.

There is no need to explicitly call Initialization/allocation of resources before use of the concurrency runtime’s critical section and release resources after the use of the critical section.

Cannot specify spin count for the concurrency runtime’s critical section object.

The concurrency runtime’s critical section enforces cooperative blocking where they yield to other cooperative tasks in the runtime when blocked.

Exceptions are thrown by the concurrency runtime’s critical section object; on unlock calls when the lock is not held, or if a lock is destroyed when being held.