To write a program of any complexity using threads, you'll need to share data between threads, or cause various actions to be performed in some coherent order across multiple threads. To do this, you need to
Section 3.1 describes a few of the basic terms we'll be using to talk about thread synchronization:
Section 3.2 describes the basic Pthreads synchronization mechanism, the mutex.
Section 3.3 describes the
Section 3.4 completes this chapter on synchronization with some important information about threads and how they view the computer's memory.
3.1 Invariants, critical sections,and predicates
'I know what you're thinking about,'
said Tweedledum; 'but it isn't so, nohow.'
'Contrariwise,' continued Tweedledee,
'if it was so, it might be; and if it were so, it would be;
but as it isn't, it ain't. That's logic.'
It is hard to write a program that doesn't have invariants, though many of them are subtle. When a program encounters a broken invariant, for example, if it dereferences a queue header containing a pointer to something that is not a valid data element, the program will probably produce incorrect results or fail immediately.
Most invariants can be 'broken,' and are routinely broken, during isolated areas of code. The trick is to be sure that broken invariants are always repaired before 'unsuspecting' code can encounter them. That is a large part of what 'synchronization' is all about in an asynchronous program. Synchronization protects your program from broken invariants. If your code locks a mutex whenever it must (temporarily) break an invariant, then other threads that rely on the invariant, and which also lock the mutex, will be delayed until the mutex is unlocked— when the invariant has been restored.
Synchronization is voluntary, and the participants must cooperate for the system to work. The programmers must agree not to fight for (or against) possession of the bailing bucket. The bucket itself does not somehow magically ensure that one and only one programmer bails at any time. Rather, the bucket is a reliable shared token that, if used properly, can allow the programmers to manage their resources effectively.
'Predicates' are logical expressions that describe the state of invariants needed by your code. In English, predicates can be expressed as statements like 'the queue is empty' or 'the resource is available.' A predicate may be a boolean variable with a TRUE or FALSE value, or it may be the result of testing whether a pointer is NULL. A predicate may also be a more complicated expression, such as determining whether a counter is greater than some threshold. A predicate may even be a value returned from some function. For example, you might call select or poll to determine whether a file is ready for input.
3.2 Mutexes
Most threaded programs need to share some data between threads. There may be trouble if two threads try to access shared data at the same time, because one thread may be in the midst of modifying some data invariant while another acts on the data as if it were consistent. This section is all about protecting the program from that sort of trouble.
The most common and general way to synchronize between threads is to ensure that all memory accesses to the same (or related) data are 'mutually exclusive.' That means that only one thread is allowed to write at a time—others must wait for their turn. Pthreads provides mutual exclusion using a special form of Edsger Dijkstra's semaphore
Experience has shown that it is easier to use mutexes correctly than it is to use other synchronization models such as a more general semaphore. It is also easy to build any synchronization models using mutexes in combination with condition variables (we'll meet them at the next corner, in Section 3.3). Mutexes are simple, flexible, and can be implemented efficiently.
The programmers' bailing bucket is something like a mutex (Figure 3.1). Both are 'tokens' that can be handed around, and used to preserve the integrity of the concurrent system. The bucket can be thought of as protecting the bailing critical section—each programmer accepts the responsibility of bailing while holding the bucket, and of avoiding interference with the current bailer while not holding the bucket. Or, the bucket can be thought of as protecting the invariant that water can be removed by only one programmer at any time.
Synchronization isn't important just when you modify data. You also need synchronization when a thread needs to read data that was written by another thread, if the order in which the data was written matters. As we'll see a little later, in Section 3.4, many hardware systems don't guarantee that one processor will see shared memory accesses in the same order as another processor without a 'nudge' from software.
FIGURE 3.1
Consider, for example, a thread that writes new data to an element in an array, and then updates a max_index variable to indicate that the array element is valid. Now consider another thread, running simultaneously on another processor, that steps through the array performing some computation on each valid element. If the second thread 'sees' the new value of max_index before it sees the new value of the array element, the computation would be incorrect. This may seem irrational, but memory systems that work this way can be substantially faster than