Ostep-Condition-Variables

Condition Variables

When threads must wait for some condition to be met, it's incredibly inefficient for them to just spin. This motivates the next primitive which is a condition variable:

// init
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
// have the current thread go to sleep w.r.t. `c`
pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
// signal to any thread waiting on `c` that it can wake up now
pthread_cond_signal(pthread_cond_t *c);

A few things to remember:

  • always check the condition: don't assume that because the thread has resumed execution that the condition is now met.
  • always hold the lock when calling signal or wait

Mesa vs. Hoare

Almost all systems today employ the Mesa semantics of signals: signaling a thread only wakes it up. It is a hint that the state of the world has changed, but it does not guarantee that the state is desirable for the thread to continue execution. In contrast, Hoare semantics provide a stronger guarantee that a thread will run immediately upon being woken, but these are harder to build and not prevalent today.

The takeaway is to always use a while loop when checking for a condition rather than an if, e.g.

cond_t empty_buffer_cond, fill_buffer_cond;
mutex_t mutex;
int buffer_size = 0;

// buffer consumer needs to wait, e.g. 
fn recv() {
	Pthread_mutex_lock(&mutex);
	while (buffer_size == 0) { // if would be incorrect!
		Pthread_cond_wait(&fill_buffer_cond, &mutex)
	}
	int val = get();
	Pthread_cond_signal(&empty_buffer_cond);
	Pthread_mutex_unlock(&mutex);
}

Covering Condition

Sometimes it may be necessary to use another primitive to broadcast to all waiting threads (for a particular condition variable) to wake up. For example, a multi-threaded memory allocator might need to signal that there is now freed memory, but it won't know which thread was requesting an amount that would fit in the freed memory. The simple solution is to simply wake all the threads to make sure that one of them gets to use it.