Prev: lock-based-concurrent-data-structures Next: semaphores
Condition variables are for cases where a thread needs to check that a certain condition has been met before continuing.
Imagine a parent, which spawns a child thread. The parent needs to wait for the child thread to finish, but obviously wouldn’t want to spin-lock.
The spin-lock version is inefficient:
volatile int done = 0;
void *child(void *arg) {
("child\n");
printf= 1;
done return NULL;
}
int main(int argc, char *argv[]) {
("parent: begin\n");
printf;
pthread_t c(&c, NULL, child, NULL); // create child
Pthread_createwhile (done == 0)
; // spin
("parent: end\n");
printfreturn 0;
}
We use condition variables, where a thread sleeps (uses no CPU) until another thread signals the desired condition and wakes the thread (like park/unpark and futex).
int done = 0;
= PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t m = PTHREAD_COND_INITIALIZER;
pthread_cond_t c
void thr_exit() {
(&m);
Pthread_mutex_lock= 1;
done (&c);
Pthread_cond_signal(&m);
Pthread_mutex_unlock}
void *child(void *arg) {
("child\n");
printf();
thr_exitreturn NULL;
}
void thr_join() {
(&m);
Pthread_mutex_lockwhile (done == 0)
(&c, &m);
Pthread_cond_wait(&m);
Pthread_mutex_unlock}
int main(int argc, char *argv[]) {
("parent: begin\n");
printf;
pthread_t p(&p, NULL, child, NULL);
Pthread_create();
thr_join("parent: end\n");
printfreturn 0;
}
The POSIX calls look like so:
(pthread_cond_t *c, pthread_mutex_t *m);
pthread_cond_wait(pthread_cond_t *c); pthread_cond_signal
CondVars require a mutex, condition var to signal, and a variable to mutate.
The Producer Consumer problem arises when producers produce data into some shared data structure and consumers read from it concurrently.
Take an example like so:
int buffer;
int count = 0; // initially, empty
void put(int value) {
(count == 0);
assert= 1;
count = value;
buffer }
int get() {
(count == 1);
assert= 0;
count return buffer;
}
void producer(void *arg) {
int i;
int loops = (int)arg;
for (i = 0; i < loops; i++) {
(i);
put}
}
void consumer(void *arg) {
while (1) {
int tmp = get();
("%d\n", tmp);
printf}
}
One broken approach might look like this:
int loops; // must initialize somewhere...
;
cond_t cond;
mutex_t mutex
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // p1
Pthread_mutex_lockif (count == 1) // p2
(&cond, &mutex); // p3
Pthread_cond_wait(i); // p4
put(&cond); // p5
Pthread_cond_signal(&mutex); // p6
Pthread_mutex_unlock}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // c1
Pthread_mutex_lockif (count == 0) // c2
(&cond, &mutex); // c3
Pthread_cond_waitint tmp = get(); // c4
(&cond); // c5
Pthread_cond_signal(&mutex); // c6
Pthread_mutex_unlock("%d\n", tmp);
printf}
}
This unfortunately only works for a single consumer and producer. If a consumer comes first, goes to sleep, then a producer puts data on the buffer, and signals to the first thread, another thread could come in its place and process the buffer.
This is because signals have Mesa Semantics
, where a
woken thread is not guaranteed to be the next thread that runs.
Hoare Semantics
would guarantee this, but these
semantics are rare.
This can be fixed by using a while
loop instead of an if
conditional.
int loops;
;
cond_t cond;
mutex_t mutex
void producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // p1
Pthread_mutex_lockwhile (count == 1) // p2
(&cond, &mutex); // p3
Pthread_cond_wait(i); // p4
put(&cond); // p5
Pthread_cond_signal(&mutex); // p6
Pthread_mutex_unlock}
}
void consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // c1
Pthread_mutex_lockwhile (count == 0) // c2
(&cond, &mutex); // c3
Pthread_cond_waitint tmp = get(); // c4
(&cond); // c5
Pthread_cond_signal(&mutex); // c6
Pthread_mutex_unlock("%d\n", tmp);
printf}
}
This, however, leads to deadlock, as none of the threads can make progress.
To solve this problem, we use two condvars, one for producers and one for consumers.
int buffer[MAX];
int fill_ptr = 0;
int use_ptr = 0;
int count = 0;
void put(int value) {
[fill_ptr] = value;
buffer= (fill_ptr + 1) % MAX;
fill_ptr ++;
count}
int get() {
int tmp = buffer[use_ptr];
= (use_ptr + 1) % MAX;
use_ptr --;
countreturn tmp;
}
, fill;
cond_t empty;
mutex_t mutex
void *producer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // p1
Pthread_mutex_lockwhile (count == MAX) // p2
(&empty, &mutex); // p3
Pthread_cond_wait(i); // p4
put(&fill); // p5
Pthread_cond_signal(&mutex); // p6
Pthread_mutex_unlock}
}
void *consumer(void *arg) {
int i;
for (i = 0; i < loops; i++) {
(&mutex); // c1
Pthread_mutex_lockwhile (count == 0) // c2
(&fill, &mutex); // c3
Pthread_cond_waitint tmp = get(); // c4
(&empty); // c5
Pthread_cond_signal(&mutex); // c6
Pthread_mutex_unlock("%d\n", tmp);
printf}
}
There is one more use for condvars, where a producer wants to broadcast to all consumers.
Imagine a situation where one thread calls malloc(100)
and another thread calls malloc(10)
and both go to sleep,
because there is not enough memory. Next, a call to
free(50)
comes back, but it doesn’t know which threads to
wake up (if it wakes up the thread calling for malloc(100)
,
it goes back to sleep).
To solve this problem, the code will instead call
pthread_cond_broadcast()
, which wakes up all waiting
threads. This guarantees that the resource at hand (bytes from the
allocator) is used efficiently, even if waking up threads may be
inefficient sometimes.
Prev: lock-based-concurrent-data-structures Next: semaphores