5.2.3 POSIX Locking

The POSIX standard allows the programmer to avoid data races via ``POSIX locking''. POSIX locking features a number of primitives, the most fundamental of which are pthread_mutex_lock() and pthread_mutex_unlock(). These primitives operate on locks, which are of type pthread_mutex_t. These locks may be declared statically and initialized with PTHREAD_MUTEX_INITIALIZER, or they may be allocated dynamically and initialized using the pthread_mutex_init() primitive. The demonstration code in this section will take the former course.

The pthread_mutex_lock() primitive ``acquires'' the specified lock, and the pthread_mutex_unlock() ``releases'' the specified lock. Because these are ``exclusive'' locking primitives, only one thread at a time may ``hold'' a given lock at a given time. For example, if a pair of threads attempt to acquire the same lock concurrently, one of the pair will be ``granted'' the lock first, and the other will wait until the first thread releases the lock.

Quick Quiz 5.8: What if I want several threads to hold the same lock at the same time? End Quick Quiz

Figure: Demonstration of Exclusive Locks
\begin{figure}{ \scriptsize
\begin{verbatim}1 pthread_mutex_t lock_a = PTHREA...
...ock'');
46 exit(-1);
47 }
48 return NULL;
49 }\end{verbatim}
}\end{figure}

This exclusive-locking property is demonstrated using the code shown in Figure [*] (lock.c). Line 1 defines and initializes a POSIX lock named lock_a, while line 2 similarly defines and initializes a lock named lock_b. Line 3 defines and initializes a shared variable x.

Lines 5-28 defines a function lock_reader() which repeatedly reads the shared variable x while holding the lock specified by arg. Line 10 casts arg to a pointer to a pthread_mutex_t, as required by the pthread_mutex_lock() and pthread_mutex_unlock() primitives.

Quick Quiz 5.9: Why not simply make the argument to lock_reader() on line 5 of Figure [*] be a pointer to a pthread_mutex_t? End Quick Quiz

Lines 12-15 acquire the specified pthread_mutex_t, checking for errors and exiting the program if any occur. Lines 16-23 repeatedly check the value of x, printing the new value each time that it changes. Line 22 sleeps for one millisecond, which allows this demonstration to run nicely on a uniprocessor machine. Line 24-27 release the pthread_mutex_t, again checking for errors and exiting the program is any occur. Finally, line 28 returns NULL, again to match the function type required by pthread_create().

Quick Quiz 5.10: Writing four lines of code for each acquisition and release of a pthread_mutex_t sure seems painful! Isn't there a better way? End Quick Quiz

Lines 31-49 of Figure [*] shows lock_writer(), which periodically update the shared variable x while holding the specified pthread_mutex_t. As with lock_reader(), line 34 casts arg to a pointer to pthread_mutex_t, lines 36-39 acquires the specified lock, and lines 44-47 releases it. While holding the lock, lines 40-48 increment the shared variable x, sleeping for five milliseconds between each increment.

Figure: Demonstration of Same Exclusive Lock
\begin{figure}{ \scriptsize
\begin{verbatim}1 printf(''Creating two threads u...
...
17 perror(''pthread_join'');
18 exit(-1);
19 }\end{verbatim}
}\end{figure}

Figure [*] shows a code fragment that runs lock_reader() and lock_writer() as thread using the same lock, namely, lock_a. Lines 2-6 create a thread running lock_reader(), and then Lines 7-11 create a thread running lock_writer(). Lines 12-19 wait for both threads to complete. The output of this code fragment is as follows:



Creating two threads using same lock:
lock_reader(): x = 0


Because both threads are using the same lock, the lock_reader() thread cannot see any of the intermediate values of x produced by lock_writer() while holding the lock.

Quick Quiz 5.11: Is ``x = 0'' the only possible output from the code fragment shown in Figure [*]? If so, why? If not, what other output could appear, and why? End Quick Quiz

Figure: Demonstration of Different Exclusive Locks
\begin{figure}{ \scriptsize
\begin{verbatim}1 printf(''Creating two threads w...
...
18 perror(''pthread_join'');
19 exit(-1);
20 }\end{verbatim}
}\end{figure}

Figure [*] shows a similar code fragment, but this time using different locks: lock_a for lock_reader() and lock_b for lock_writer(). The output of this code fragment is as follows:



Creating two threads w/different locks:
lock_reader(): x = 0
lock_reader(): x = 1
lock_reader(): x = 2
lock_reader(): x = 3


Because the two threads are using different locks, they do not exclude each other, and can run concurrently. The lock_reader() function can therefore see the intermediate values of x stored by lock_writer().

Quick Quiz 5.12: Using different locks could cause quite a bit of confusion, what with threads seeing each others' intermediate states. So should well-written parallel programs restrict themselves to using a single lock in order to avoid this kind of confusion? End Quick Quiz

Quick Quiz 5.13: In the code shown in Figure [*], is lock_reader() guaranteed to see all the values produced by lock_writer()? Why or why not? End Quick Quiz

Quick Quiz 5.14: Wait a minute here!!! Figure [*] didn't initialize shared variable x, so why does it need to be initialized in Figure [*]? End Quick Quiz

Although there is quite a bit more to POSIX exclusive locking, these primitives provide a good start and are in fact sufficient in a great many situations. The next section takes a brief look at POSIX reader-writer locking.

Paul E. McKenney 2011-12-16