CS 321 Spring 2013 > Lecture Notes for Wednesday, February 6, 2013 |
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.
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 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.
count_lock.lock(); if (count < 3) ++count; count_lock.unlock();
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) ++count; count_lock.unlock(); } else { 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.
ggchappell@alaska.edu