CS 321 Spring 2013  >  Lecture Notes for Wednesday, February 6, 2013

CS 321 Spring 2013
Lecture Notes for Wednesday, February 6, 2013

Threads (cont’d) [2.2]

Nondeterminism & Race Conditions

Programs thread1.cpp (NetRun link) and thread2.cpp (NetRun link) may not produce the same output each time they are run. Their behavior depends on many little decisions made by the process/thread scheduler, which in turn depend on all kinds of factors unrelated to the programs. This is an example of nondeterminism: a nondeterministic program has results that do not depend only on its input.

Nondeterminism is a common property of multithreaded programs. This fact has led some people to say that, if you create multiple threads, then you are no longer really programming a computer, as a computer is a deterministic device.

Sometimes we do not care much whether the output of a multithreaded program can vary; sometimes we care a great deal. Recall that, when the correctness of a program depends on scheduler decisions, we have a race condition. Race conditions can have important implications for debugging; imagine a bug that shows up intermittently, and only on some machines.

A Race-Condition Story

Here is a famous example of a race condition that caused serious harm. Information is taken from “The THERAC-25 Accidents”, Appendix A of Safeware: System Safety and Computers by Nancy G. Leveson, Addison-Wesley, 1995.

The THERAC-25 was a medical radiation-therapy device made in the 1970s and 1980s by Atomic Energy of Canada Limited. It was designed to destroy cancerous tumors by irradiating them either with an electron beam or with x-rays.

In the first (electron-beam) mode, a low-power electron beam was directed at the tumor. In the second (x-ray) mode, an equipment assembly that included a metal “target” was placed in the path of a high-power electron beam, generating x-rays which were directed at the tumor.

The system was controlled by a computer running multithreaded software. (The computer is described as having multiple “tasks” executing concurrently and using shared memory; so I call them “threads”.) Various threads controlled actions such as changing the power of the electron beam, positioning the target assembly either in the beam or not, and communicating with the operator’s terminal. Due to a race condition in the software, if an operator entered certain input very quickly, then the beam could be set to high power without the target assembly in place. When this happened, a patient would be directly irradiated with a high-power electron beam.

This not being a physics or medicine class, we will not go into details. Suffice it to say that this is BAD. As a result of the race condition, six people were seriously injured, five of them permanently. Three of them died as a result of these injuries.

Race conditions are serious business; watch out for them.

A Little about Mutexes

A mutex (the term comes from mutual exclusion) a facility for regulating access to a shared resource. It is designed to allow only one thread to access a resource at any given time. Mutexes offer a straightforward way to eliminate data races. Proper use of mutexes can also prevent some race conditions (however, this is not a foolproof way to prevent all race conditions). We will discuss mutexes in more detail later in the semester; for now, we look at then mutexes in the C++11 threads package.

A critical section in a program is a section of code that performs some action—for example, accessing a shared resource—that must only be done by one thread at a time. We use a mutex to acquire a lock at the beginning of a critical section, and release the lock when we exit the section. The mutex insures that only one thread has the lock at one time; thus only one thread can be in a critical section at one time.

Suppose a thread is about to enter a critical section. It requests the lock. If the lock is not held by some other thread, then this thread acquires the lock, executes the critical section, and releases it. If the lock is held by some other thread, then this thread is added to a list (perhaps a queue) of threads that have requested the lock. When the lock is released, one of the threads in the list is allowed to acquire it, and that thread is removed from the list. Eventually, our thread acquires the lock, executes the critical section, and releases the lock.

In the C++11 threads package, mutexes are implemented as objects of type std::mutex, which is declared in the header <mutex>.

#include <mutex>
using std::mutex;

mutex count_lock;

When we use a mutex, we usually think of it as being associated with some piece of shared data. In my example, I will have a shared variable named count; I will regulate access to count using the above mutex, which I have named count_lock to remind me what it is associated with.

int count = 0;

When multiple threads are running, any access to count is a critical section. When a thread enters a critical section, it should first acquire the lock by calling member function lock of count_lock. When it exits the critical section, it releases the lock by calling member function unlock. These functions both have no parameters and no return value.

if (count < 3)

If locking is done consistently, then the above critical section is atomic (with respect to accesses to count).

The C++11 threads package also allows for a non-blocking lock request. Member function try_lock of class mutex takes no parameters and returns a bool. It requests the lock. If the lock is available, then it acquires the lock and returns true. If the lock is unavailable, then it simply returns false, without adding the thread to the list of requesters.

bool gotlock = count_lock.try_lock();
if (gotlock)
    if (count < 3)
    cout << "I could not get the lock. Shucks." << endl;

Note that there is no formal association between the mutex and the shared resource being regulated. For example, above, there is nothing to prevent a thread from accessing variable count without first acquiring the lock—nothing except a programmer’s decision not to write code that does that. We thus say that a mutex gives us an advisory lock; this is in contrast to a mandatory lock.

See thread3.cpp (NetRun link) for a multithreaded program with a race condition.

See thread4.cpp (NetRun link) for a similar program in which the race condition is fixed using a mutex and blocking lock requests.

See thread5.cpp (NetRun link) for a similar program in which the race condition is partially fixed using a mutex and non-blocking lock requests.

CS 321 Spring 2013: Lecture Notes for Wednesday, February 6, 2013 / Updated: 6 Feb 2013 / Glenn G. Chappell / ggchappell@alaska.edu