CSC 357 Lecture Notes Week 10, Part 2
Details of Thread Synchronization
Final Exam Review



  1. Using mutex locks.
    1. As introduced in Part 1 of these notes, a mutex is provides the means to lock a resource for use exclusively by a single thread.
    2. When other threads try to acquire the mutex lock, they block until the thread holding the lock releases it.
    3. A common use of mutex locks is to maintain the consistency of shared data structures, as illustrated in the example on pages 371-373 of Stevens.
    4. As another example, consider the use of a stack data structure, shared among two or more threads.
      1. The operations on the stack should be "thread safe".
      2. Thread-safe means if two or more threads simultaneously call a stack operation, the state of the stack remains consistently correct.
      3. Consider a standard implementation of a stack push operation:

        void push(Stack s, Value v) {
            s[cur_index] = v;
            cur_index++;
        }
        
      4. Now consider the following sequence of actions:
        1. Thread A calls push(some_stack, some_value).
        2. Around the same time, Thread B calls push(some_stack, some_other_value).
        3. The push operation starts in response to Thread A's call.
        4. After executing s[cur_index] = v, but before getting to cur_index++, the push operation starts in response to Thread B's call.
        5. Since cur_index has not yet been incremented by Thread A's call, Thread B's call clobbers the stack value at cur_index assigned by Thread A's call.
      5. A mutex can be used to make push thread-safe, aka, reentrant, as follows:

        pthread_mutex_t stack_lock = PTHREAD_MUTEX_INITIALIZER;
        
        ...

        void push(Stack s, Value v) { pthread_mutex_lock(&stack_lock); s[cur_index] = v; cur_index++; pthread_mutex_unlock(&stack_lock); }


  2. Using condition variables.
    1. A condition variable is a kernel-managed datum used to support waiting and signaling among threads.
    2. There are two key functions associated with condition variables:
      1. int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
      2. int pthread_cond_signal(pthread_cond_t *cond)
    3. There are other functions, but these provide the core functionality.
    4. Here is a general pattern of use for these functions (see also pp. 382-385 of Stevens):
      Shared Data:
      pthread_cond_t c = PTHREAD_MUTEX_INITIALIZER;
      pthread_mutex_t m = PTHREAD_COND_INITIALIZER;
      bool some_condition;



      Thread A:                              Thread B:
          ...                                    ...
      pthread_mutex_lock(&m);                pthread_mutex_lock(&m);
      while (!some_condition) {              some_condition = TRUE;
          pthread_cond_wait(&c, &m);         pthread_mutex_unlock(&m);
      }                                      pthread_cond_signal(&c);
      do_A_thing();
      some_condition = FALSE;                      ...
      pthread_mutex_unlock(&m);


          ...
    5. The intended behavior of this pattern is for Thread A to wait for some_condition to become true before doing its thing, i.e., calling do_A_thing.
    6. When using condition variables, there is always a boolean predicate associated with each condition wait.
      1. After returning from a pthread_cond_wait, the predicate should be true before the waiting thread proceeds (thread A in the preceding pattern).
      2. Typically, the predicate is made true by another cooperating thread (thread B in the pattern).
      3. The predicate can be implemented as a shared boolean variable, or a boolean test on the value of a shared variable.
      4. The setting and testing of the predicate are always done in tandem with the calls to pthread_cond_signal and pthread_cond_wait.
      5. The condition variable itself is not the value used in the predicate; rather the condition variable is a black-box object used by the kernel to implement the waiting and signaling functionality.
    7. The specification of pthread_cond_wait requires that the mutex sent as the second argument be locked before the call, otherwise pthread_cond_wait can produce undefined behavior.
    8. Furthermore, not locking the condition mutex before the predicate test can lead to lost wake-up bugs.
      1. A lost wake-up occurs when:
        1. A thread calls pthread_cond_signal.
        2. Another thread is between the test of the condition predicate and the call to pthread_cond_wait.
        3. No other threads are waiting.
      2. Under these circumstances, the signal has no effect, and therefore is lost.
    9. The precise behavior of pthread_cond_wait is as follows:
      1. The call to pthread_cond_wait blocks the calling thread (Thread A) until another thread (Thread B) calls pthread_cond_signal.
      2. Before the wait commences, pthread_cond_wait releases the entering mutex, so that other threads can acquire it.
      3. Just before pthread_cond_wait returns, it re-locks the mutex on behalf of the calling thread, so the calling thread must explicitly unlock it when appropriate.


    10. Note that the while loop around the pthread_cond_wait is not a "busy" wait, since almost all of the waiting time is spent within the kernel's implementation of pthread_cond_wait.
      1. One might think that a simple if test would suffice, since the condition predicate should always be set true prior to calling pthread_cond_signal.
      2. The while loop is necessary to account for so-called spurious wakeups, which cause pthread_cond_wait to return without another thread having explicitly called pthread_cond_signal.
      3. Such spurious wakeups can occur when a thread receives a signal during a pthread_cond_wait.
      4. Spurious wakeups can also occur with the use of the pthread_cond_broadcast function, which unblocks multiple threads waiting on a pthread_cond_wait.

  3. Final quiz and exam review -- see the handout.



index | lectures | labs | programs | handouts | solutions | examples | documentation | bin