by Andrew D. Birrell
summarized by Adam M. Costello
[Note: The paper is presented in terms of the particular thread facility used in DEC SRC. The basic design has become conventional, so this summary uses a more neutral presentation, but retains the SRC terminology.]
A thread is a sequential flow of control. Each thread has its own program counter and call stack, but multiple threads may share an address space containing global variables, which can be used for communication among the threads.
Threads are one way (not the only way) to achieve concurrency, which is useful for:
One thread can create another via a fork operation. Some thread facilities also provide a join operation to wait for a thread to terminate.
To avoid incorrectness caused by multiple threads accessing the same data structures at the same time, programmers can use monitors, invented by Tony Hoare. Monitors are based on mutexes and condition variables.
A mutex is an object that can be held by at most one thread at a time. A thread attempting to lock (acquire) a mutex that is already held will block until the thread scheduler gives the mutex to the blocked thread.
A condition variable is like a mutex in that threads can block on them, but condition variables are not held. The wait(m,c) operation atomically unlocks a mutex and blocks on a condition variable. The signal(c) operation chooses one or more threads currently blocked on a particular condition variable, and unblocks them. The broadcast(c) operation is the same except that it unblocks all the threads waiting on the condition variable. When a waiting thread is signaled, the wait operation does not return until it has relocked the mutex (possibly blocking on the mutex).
A monitor consists of some shared data, a mutex, and zero or more condition variables. Each condition variable is always used with the same mutex, but I don't know whether that is a programming convention or an actual requirement.
A monitor is typically used as follows. A thread desiring access to a “resource” locks the associated mutex and examines the associated data. If the resource is “available”, the thread performs its task, then unlocks the mutex. If the resource was unavailable, the thread unlocks the mutex and blocks on a condition variable (via wait). When another thread alters the data in a way that might render the resource available, it reawakens the first thread (via signal or broadcast), which relocks the mutex and rechecks the shared data.
lock(m)
while not available(data)
wait(m,c)
endwhile
access(data)
unlock(m)
It is important to use while
rather than
if
, because other threads may lock the mutex between the
signal and the return from the wait, and because it makes the code
locally verifiable.
Some thread facilities provide an alert operation to allow one thread to request than another terminate early.
Concurrency introduces new challenges in terms of safety (correctness), liveness (progress), and performance.
To insure safety, all shared mutable data must be associated with a mutex, and all threads must hold the mutex while reading or writing the data. For complex data structures, it is helpful to associate an invariant (a boolean function of the data structure) with each mutex. The truth of the invariant is the only assumption a thread may make about the data when it locks the mutex, and the thread is responsible for restoring the truth of the invariant before it unlocks the mutex (including waits).
To insure liveness, deadlock must be avoided. A deadlock is a state in which all threads are blocked, so none will ever proceed. This can happen when two threads each hold a resource, and each thread is trying to acquire the other resource. The easiest way to avoid this situation is to order the resources, and require all threads to acquire resources in order. Sometimes it is easier to impose this order if the resources are divided up at a finer granularity.
The other threat to liveness is starvation, a situation in which a thread waits forever for a resource. Avoiding starvation depends on the particular scheduling policies used by the thread facility, and on the policies used by the programming in deciding when to wait and signal.
There are many potential threats to performance:
These threats can all be dealt with, at the cost of increasing complexity, which increases the risk of incorrectness, so the programmer should use restraint.
There are some common techniques for using threads:
|