Introduction

Are you hearing this term for the first time ? Or maybe you’ve heard it before but never really understood it ? Or maybe you’ve banged your head against the wall trying to understand it numerous times ?

Well, you’re in luck. This post is for you.

Condition variables are a synchronization primitive in POSIX (Portable Operating System Interface) systems that allow threads to wait for certain conditions to become true. Unlike mutexes, which are used for mutual exclusion, condition variables allow threads to pause execution and wait, without consuming CPU resources, until a specific condition changes state. This mechanism is particularly useful in scenarios where a thread needs to wait for some condition to be met by another thread before proceeding.

Raise Your Hand if you didn’t understand that. (I’m Just Kidding. (or am I ?))

Don’t worry, let’s start with an example.

Functions

The following functions are used to work with condition variables in POSIX systems:

  • pthread_cond_init: Initializes a condition variable.
  • pthread_cond_destroy: Destroys a condition variable.
  • pthread_cond_wait: Waits on a condition variable.
  • pthread_cond_signal: Signals a condition variable.
  • pthread_cond_broadcast: Signals a condition variable and wakes up all waiting threads.

Example

  • Imagine you want to create a lock such that it can allow multiple users to read the data but only one user can write the data at a time.
  • We will basically have 2 signals (condition_variables) for readers and writers. Readers would wait for the signal to be given by writers that “Hey’ I’m done writing, you can read now” and writers would wait for the signal to be given by readers that “Hey, I’m done reading, you can write now”.
  • We have the following struct for the read-write lock
struct rwlock {
	int readers;
	int writers;
	int read_waiters;
	int write_waiters;
	pthread_mutex_t lock;
	pthread_cond_t read_condvar; // signal for readers
	pthread_cond_t write_condvar; // signal for writers
};
  • If we want to acquire the read lock, we would do the following
int rwlock_rdlock_lock(struct rwlock* rwlock){
	// if there are no writers, we can proceed normally
	pthread_mutex_lock(&rwlock->lock);
	if(rwlock->writers == 0){
		rwlock->readers++; // simply increment the number of readers.
	} else {
		// wait till the signal on read_condvar given by writers
		rwlock->read_waiters++;
		// This is where we are waiting for the signal from the writers
		while(pthread_cond_wait(&rwlock->read_condvar, &rwlock->lock) != 0);
		rwlock->read_waiters--;
		// can increment readers now
		rwlock->readers++; 
	}
	return pthread_mutex_unlock(&rwlock->lock);
}
  • Similarly, for acquiring the write lock, we would do the following
int rwlock_wrlock_lock(struct rwlock* rwlock){
	pthread_mutex_lock(&rwlock->lock);
	if(rwlock->readers == 0 && rwlock->writers == 0){
		rwlock->writers = 1;
	} else {
		// either some people are reading, then we must wait for the signal on write_cond var
		// or there is a writer, even then we must wait for the signal on write_cond var
		// in either case wait for the signal on write_cond var
		rwlock->write_waiters++;
		while(pthread_cond_wait(&rwlock->write_condvar, &rwlock->lock) != 0);
		rwlock->write_waiters--;
		rwlock->writers = 1;
		// we have obtained the lock now
	}
	pthread_mutex_unlock(&rwlock->lock);
};
  • Basically, the idea is that we are waiting for the signal to be given by the other party before proceeding. This is the basic idea behind condition variables.

  • You might be wondering but when do we signal the other party ? The answer is whenever we are done with or work and call the unlock function.

int rwlock_rdlock_unlock(struct rwlock* rwlock){
	pthread_mutex_lock(&rwlock->lock);
	if(rwlock->readers > 1) rwlock->readers--;
	else if(rwlock->readers == 1) {
		rwlock->readers = 0;
		// people might be waiting for us. Let's give priority to readers
		if(rwlock->read_waiters > 0){
			pthread_cond_broadcast(&rwlock->read_condvar);
		} else if(rwlock->write_waiters > 0){
			pthread_cond_broadcast(&rwlock->write_condvar);
		}
	}
	return pthread_mutex_unlock(&rwlock->lock);
};
  • Here you also have the liberty to choose who to give priority to. In this case, we are giving priority to readers.
  • Similarly, when we call the write_unlock the following happens
int rwlock_wrlock_unlock(struct rwlock* rwlock){
	pthread_mutex_lock(&rwlock->lock);
	rwlock->writers = 0;
	if(rwlock->read_waiters > 0){
		pthread_cond_broadcast(&rwlock->read_condvar);
	} else if(rwlock->write_waiters > 0){
		pthread_cond_broadcast(&rwlock->write_condvar);
	}
	pthread_mutex_unlock(&rwlock->lock);
}