Conditional Variables and Thread Synchronization * Multithreaded application architectures: instead of multiple processes, an application benefits from having multiple threads that share memory. When one of the threads blocks, the others can make progress. A common multithreaded architecture is a master/worker model. A master process/thread serves as an entry point, and distributes work amongst multiple worker threads. For example, in a web server, a master thread accepts new connections, and distributes each connection to a worker thread. The number of workers can be variable (e.g., one for each request), but usually a fixed pool of worker threads is used to avoid overhead of thread creation. Also, threads in an application can either run to completion (one thread handles all the different steps of processing a single request) or can be pipelined (with different threads doing different kinds of work). * A master and worker model requires coordination between the threads using thread synchronization techniques. Similarly, a pipelined design also requires signaling between stages. Conditional variables are used to achieve this synchronization between threads. pthreads provides support for conditional variables. * Conditional waiting: one thread must wait for another thread to finish some task. In such cases, synchronization/coordination can be achieved by pthreads wait/signal functions. The call to the wait function blocks and returns when another thread calls signal. * The two steps of checking for a condition and waiting on it must happen in an atomic fashion, to avoid a signal happening in between, leading to a lost wakeup. Therefore, the pthreads library conditional variable functions must be invoked along with a mutex. The mutex is unlocked by the OS once the process blocks. lock(mutex) if(not condition) // line A wait(condvar, mutex) // line B The code that calls signal must also acquire the lock and signal, guaranteering that the wakeup cannot happen between lines A and B above. * When a wakeup/signal happens, either one thread or all can be woken up, depending on the OS implementation of conditional variables. Some libraries also provide a signal broadcast function. It is always wise to check that the condition is true after waking up, using a while loop. while(not condition) wait(condvar, mutex) * Example code for producer/consumer queue with conditional variables: Code for producer: lock(mutex) while(itemCount >= MAX) wait(buffer_has_space, mutex) add item to buffer itemCount++ signal(buffer_has_data) unlock(mutex) Code for consumer: lock(mutex) while(itemCount == 0) wait(buffer_has_data, mutex) remove item from buffer to consume itemCount-- signal(buffer_has_space) unlock(mutex) * The producer consumer problem is a generic pattern that appears in many multithreaded programs. For example, producer/consumer buffers can be present to pass requests between masters and workers in a master/worker architecture, or between pipeline stages in a pipelined architecture. * Another example: consider three threads A, B, C of an application that do three different tasks. When a request arrives (into a data structure seen by all three), the threads must act on the request in the order A, B, C. The above pipelined problem can also be solved using conditional variables, using three conditional variables and three boolean variables (to signal who is done). The logic of A would be lock(mutex) while(not cdone) wait(condvarA, mutex) cdone = false //reset do A's work adone = true signal(condvarB) unlock(mutex) The logic of B and C would be analogous.