condvar operations:
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished; // data, only accessed with after acquiring lock
pthread_cond_t finished_cv; // to wait for 'finished' to be true
void WaitForFinished() {
pthread_mutex_lock(&lock);
while (!finished) {
pthread_cond_wait(&finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish() {
pthread_mutex_lock(&lock);
finished = true;
pthread_cond_broadcast(&finished_cv);
pthread_mutex_unlock(&lock);
}
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished; // data, only accessed with after acquiring lock
pthread_cond_t finished_cv; // to wait for 'finished' to be true
void WaitForFinished() {
pthread_mutex_lock(&lock);
while (!finished) {
pthread_cond_wait(&finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish() {
pthread_mutex_lock(&lock);
finished = true;
pthread_cond_broadcast(&finished_cv);
pthread_mutex_unlock(&lock);
}
acquire lock before reading or write finished
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished; // data, only accessed with after acquiring lock
pthread_cond_t finished_cv; // to wait for 'finished' to be true
void WaitForFinished() {
pthread_mutex_lock(&lock);
while (!finished) {
pthread_cond_wait(&finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish() {
pthread_mutex_lock(&lock);
finished = true;
pthread_cond_broadcast(&finished_cv);
pthread_mutex_unlock(&lock);
}
check whether we need to wait at all
yes, must be a loop — we’ll explain later
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished; // data, only accessed with after acquiring lock
pthread_cond_t finished_cv; // to wait for 'finished' to be true
void WaitForFinished() {
pthread_mutex_lock(&lock);
while (!finished) {
pthread_cond_wait(&finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish() {
pthread_mutex_lock(&lock);
finished = true;
pthread_cond_broadcast(&finished_cv);
pthread_mutex_unlock(&lock);
}
know we need to wait
(finished cannot have changed since we checked because of lock)
so wait releasing lock
important that we release lock, so finished can change
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished; // data, only accessed with after acquiring lock
pthread_cond_t finished_cv; // to wait for 'finished' to be true
void WaitForFinished() {
pthread_mutex_lock(&lock);
while (!finished) {
pthread_cond_wait(&finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish() {
pthread_mutex_lock(&lock);
finished = true;
pthread_cond_broadcast(&finished_cv);
pthread_mutex_unlock(&lock);
}
all waiters to proceed once we release lock
| WaitForFinish thread | Finish thread |
|---|---|
mutex_lock(&lock)
|
|
| (thread has lock) | |
mutex_lock(&lock)
|
|
| (start waiting for lock) | |
while (!finished) ...
|
|
cond_wait(&finished_cv, &lock);
|
|
| (start waiting for cv) | (done waiting for lock) |
finished = true
|
|
cond_broadcast(&finished_cv)
|
|
| (done waiting for cv) | |
| (start waiting for lock) | |
mutex_unlock(&lock)
|
|
| (done waiting for lock) | |
while (!finished) ...
|
|
| (finished now true, so return) | |
mutex_unlock(&lock)
|
|
| WaitForFinish thread | Finish thread |
|---|---|
mutex_lock(&lock)
|
|
finished = true
|
|
cond_broadcast(&finished_cv)
|
|
mutex_unlock(&lock)
|
|
mutex_lock(&lock)
|
|
while (!finished) ...
|
|
| (finished now true, so return) | |
mutex_unlock(&lock)
|
|
we only broadcast if finished is true
so why check finished afterwards?
pthread_cond_wait manual page:
spurious wakeup = wait returns even though nothing happened
shared buffer (queue) of fixed size
producer(s) and consumer(s) don’t work in lockstep
example: C compiler
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
rule: never touch buffer without acquiring lock
otherwise: what if two threads simulatenously en/dequeue?
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
check if empty, then dequeue
(note: have lock — don’t need to worry about someone else dequeuing)
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
check if empty (waiting if needed), then dequeue
(note: have lock — don’t need to worry about someone else dequeuing)
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
after enqueuing
wake one Consume thread (if any are waiting)
pthread_mutex_t lock;
pthread_cond_t data_ready;
Queue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
while loop could run 0 times
| Thread 1 | Thread 2 |
|---|---|
| Produce() | |
| … lock | |
| … enqueue | |
| … signal | |
| … unlock | |
| Consume? | |
| … lock | |
| … empty? no | |
| … dequeue | |
| … unlock | |
| return |
while loop could run 1 times
| Thread 1 | Thread 2 |
|---|---|
| Consume? | |
| … lock | |
| … empty? yes | |
| … unlock/start wait | |
| Produce() | |
| … lock | |
| … enqueue | |
| … signal | stop wait |
| … unlock | lock |
| … empty? no | |
| … dequeue | |
| … unlock | |
| return |
while loop could run 2+ times
| Thread 1 | Thread 2 | Thread 3 |
|---|---|---|
| Consume? | ||
| … lock | ||
| … empty? yes | ||
| … unlock/start wait | ||
| Produce() | ||
| … lock | ||
| … enqueue | ||
| … signal | stop wait | |
| … unlock | lock | |
| (waiting for lock) | Consume? | |
| … lock | ||
| … empty? no | ||
| … dequeue | ||
| … unlock | ||
| return | ||
| … empty? no | ||
| … dequeue | ||
| … unlock | ||
| return |
Hoare-style monitors
Mesa-style monitors
pthread_mutex_t lock;
pthread_cond_t data_ready;
pthread_cond_t space_ready;
BoundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
while (buffer.full()) {
pthread_cond_wait(&space_ready, &lock);
}
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_cond_signal(&space_ready);
pthread_mutex_unlock(&lock);
return item;
}
pthread_mutex_t lock;
pthread_cond_t data_ready;
pthread_cond_t space_ready;
BoundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
while (buffer.full()) {
pthread_cond_wait(&space_ready, &lock);
}
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_cond_signal(&space_ready);
pthread_mutex_unlock(&lock);
return item;
}
add new condition variable
for new reason to wait
wait on that condition
while reason to wait is true
signal that condition
when one thread can stop waiting
pthread_mutex_t lock;
pthread_cond_t data_ready;
pthread_cond_t space_ready;
BoundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
while (buffer.full()) {
pthread_cond_wait(&space_ready, &lock);
}
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_cond_signal(&space_ready);
pthread_mutex_unlock(&lock);
return item;
}
signal better than broadcast in this case
only one waiting thread can do something useful
correct (but slow) to broadcast instead
pthread_mutex_t lock;
pthread_cond_t data_ready;
pthread_cond_t space_ready;
BoundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
while (buffer.full()) {
pthread_cond_wait(&space_ready, &lock);
}
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_cond_signal(&space_ready);
pthread_mutex_unlock(&lock);
return item;
}
correct but slow to use just one condition variable:
data_ready, space_ready \(\Rightarrow\) ready
always use broadcast
pthread_mutex_lock(&lock);
while (!condition A) {
pthread_cond_wait(&condvar_for_A, &lock);
}
... /* manipulate shared data, changing other conditions */
if (set condition A) {
pthread_cond_broadcast(&condvar_for_A);
/* or signal, if only one thread cares */
}
if (set condition B) {
pthread_cond_broadcast(&condvar_for_B);
/* or signal, if only one thread cares */
}
...
pthread_mutex_unlock(&lock)never touch shared data without holding the lock
keep lock held for entire operation:
create condvar for every kind of scenario waited for
always write loop calling cond_wait to wait for condition X
broadcast/signal condition variable every time you change X
correct but slow to…
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished[2];
pthread_cond_t both_finished_cv;
void WaitForBothFinished() {
pthread_mutex_lock(&lock);
while (_____________________________) {
pthread_cond_wait(&both_finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish(int index) {
pthread_mutex_lock(&lock);
finished[index] = true;
_____________________________________
pthread_mutex_unlock(&lock);
}
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished[2];
pthread_cond_t both_finished_cv;
void WaitForBothFinished() {
pthread_mutex_lock(&lock);
while (_____________________________) {
pthread_cond_wait(&both_finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish(int index) {
pthread_mutex_lock(&lock);
finished[index] = true;
_____________________________________
pthread_mutex_unlock(&lock);
}
A. finished[0] && finished[1]
B. finished[0] || finished[1]
C. !finished[0] || !finished[1]
D. finsihed[0] != finished[1]
E. something else
// MISSING: init calls, etc.
pthread_mutex_t lock;
bool finished[2];
pthread_cond_t both_finished_cv;
void WaitForBothFinished() {
pthread_mutex_lock(&lock);
while (_____________________________) {
pthread_cond_wait(&both_finished_cv, &lock);
}
pthread_mutex_unlock(&lock);
}
void Finish(int index) {
pthread_mutex_lock(&lock);
finished[index] = true;
_____________________________________
pthread_mutex_unlock(&lock);
}
A. pthread_cond_signal(&both_finished_cv)
B. pthread_cond_broadcast(&both_finished_cv)
C. if (finished[1-index])
pthread_cond_signal(&both_finished_cv);
D. if (finished[1-index])
pthread_cond_broadcast(&both_finished_cv);
E. something else
struct BarrierInfo {
pthread_mutex_t lock;
int total_threads; // initially total # of threads
int number_reached; // initially 0
___________________
};
void BarrierWait(BarrierInfo *b) {
pthread_mutex_lock(&b->lock);
++b->number_reached;
if (b->number_reached == b->total_threads) {
_____________________
} else {
_____________________
_____________________
}
pthread_mutex_unlock(&b->lock);
}struct BarrierInfo {
pthread_mutex_t lock;
int total_threads; // initially total # of threads
int number_reached; // initially 0
pthread_cond_t cv;
};
void BarrierWait(BarrierInfo *b) {
pthread_mutex_lock(&b->lock);
++b->number_reached;
if (b->number_reached == b->total_threads) {
pthread_cond_broadcast(&b->cv);
} else {
while (b->number_reached < b->total_threads)
pthread_cond_wait(&b->cv, &b->lock);
}
pthread_mutex_unlock(&b->lock);
}makes implementing condition variables simpler
can be hard to avoid loop in more complicated scenarios
e.g. signal() saying okay to remove item from queue
what if another thread sneaks in and does it first?
pthread_mutex_t lock; pthread_cond_t data_ready; UnboundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
/* GOOD CODE: pthread_cond_signal(&data_ready); */
/* BAD CODE: */ if (buffer.size() == 1) pthread_cond_signal(&item);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {X\\tikzmark{empty}X
pthread_cond_wait(&data_ready, &lock);
}X\\tikzmark{after loop}X
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
| thread 0 | 1 | 2 | 3 |
| \hline{} Consume(): | |||
| lock | |||
| empty? wait on cv | Consume(): | ||
| lock | |||
| empty? wait on cv | |||
| Produce(): | |||
| lock | Produce(): | ||
| thread 0 | 1 | 2 | 3 |
| \hline{} Consume(): | |||
| lock | |||
| empty? wait on cv | Consume(): | ||
| lock | |||
| empty? wait on cv | |||
| Produce(): | |||
| lock | Produce(): | ||
| wait for lock | |||
| enqueue | |||
| wait for lock | size = 1? signal | ||
| unlock | gets lock | ||
| enqueue | |||
| size \(\not=\) 1: don’t signal | |||
| unlock | |||
| gets lock | |||
| dequeue | |||
| still waiting |
suppose we want producer/consumer, but…
but change Consume() to ConsumeTwo() which returns a pair of values
what should we change below?
pthread_mutex_t lock;
pthread_cond_t data_ready;
UnboundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
(one of many possible solutions)
Assuming ConsumeTwo replaces Consume:
Produce() {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
if (buffer.size() > 1) { pthread_cond_signal(&data_ready); }
pthread_mutex_unlock(&lock);
}
ConsumeTwo() {
pthread_mutex_lock(&lock);
while (buffer.size() < 2) { pthread_cond_wait(&data_ready, &lock); }
item1 = buffer.dequeue(); item2 = buffer.dequeue();
pthread_mutex_unlock(&lock);
return Combine(item1, item2);
}
(one of many possible solutions)
Assuming ConsumeTwo is in addition to Consume (using two CVs):
Produce() {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&one_ready);
if (buffer.size() > 1) { pthread_cond_signal(&two_ready); }
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.size() < 1) { pthread_cond_wait(&one_ready, &lock); }
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
ConsumeTwo() {
pthread_mutex_lock(&lock);
while (buffer.size() < 2) { pthread_cond_wait(&two_ready, &lock); }
item1 = buffer.dequeue(); item2 = buffer.dequeue();
pthread_mutex_unlock(&lock);
return Combine(item1, item2);
}
(one of many possible solutions)
Assuming ConsumeTwo is in addition to Consume (using one CV):
Produce() {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
// broadcast and not signal, b/c we might wakeup only ConsumeTwo() otherwise
pthread_cond_broadcast(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.size() < 1) { pthread_cond_wait(&data_ready, &lock); }
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
ConsumeTwo() {
pthread_mutex_lock(&lock);
while (buffer.size() < 2) { pthread_cond_wait(&data_ready, &lock); }
item1 = buffer.dequeue(); item2 = buffer.dequeue();
pthread_mutex_unlock(&lock);
return Combine(item1, item2);
}
pthread_mutex_t lock; pthread_cond_t cv;
bool FirstFinished = false; bool SecondFinished = false;
void FinishFirst() {
pthread_mutex_lock(&lock);
FirstFinished = true;
____________________ // (1)
pthread_mutex_unlock(&lock);
}
void FinishSecond() {
pthread_mutex_lock(&lock);
SecondFinished = true;
____________________ // (1)
pthread_mutex_unlock(&lock);
}
void WaitForBothFinished() {
pthread_mutex_lock(&lock);
___ ( ____________________________ ) { // (2)
pthread_cond_wait(&lock, &cv);
}
pthread_mutex_unlock(&lock);
}
Fill in the blanks.
pthread_mutex_t lock;
pthread_cond_t data_ready;
UnboundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
pthread_mutex_unlock(&lock);
return item;
}
(one of many possible solutions)
:::: {.columns}
struct Waiter {
pthread_cond_t cv;
bool done;
T item;
}
Queue<Waiter*> waiters;
Produce(item) {
pthread_mutex_lock(&lock);
if (!waiters.empty()) {
Waiter *waiter = waiters.dequeue();
waiter->done = true;
waiter->item = item;
cond_signal(&waiter->cv);
++num_pending;
} else {
buffer.enqueue(item);
}
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
if (buffer.empty()) {
Waiter waiter;
cond_init(&waiter.cv);
waiter.done = false;
waiters.enqueue(&waiter);
while (!waiter.done)
cond_wait(&waiter.cv, &lock);
item = waiter.item;
} else {
item = buffer.dequeue();
}
pthread_mutex_unlock(&lock):
return item;
}
::::
pthread_mutex_t lock;
pthread_cond_t data_ready; pthread_cond_t @4space_ready4@;
BoundedQueue buffer;
Produce(item) {
pthread_mutex_lock(&lock);
while (buffer.full()) { pthread_cond_wait(@4&space_ready4@, &lock); }
buffer.enqueue(item);
pthread_cond_signal(&data_ready);
pthread_mutex_unlock(&lock);
}
Consume() {
pthread_mutex_lock(&lock);
while (buffer.empty()) {
pthread_cond_wait(&data_ready, &lock);
}
item = buffer.dequeue();
[[pthread_cond_signal(@4&space_ready4@);]{.fragment fragment-index=2 .custom .myem-only}]{.fragment fragment-index=3 .custom .myem-only}X\\tikzmark{signal}X
pthread_mutex_unlock(&lock);
return item;
}
unconditionally signal
broadcast if buffer changed from full to not-full
explicitly count number of waiting producers — buffer not full and waiter
question: who might be waiting when condition changes
almost always multiple threads!
if not broadcasting, explain why each waiting thread gets to go
my implicit non-explanation: queue will be full again first
alternate view: consuming causes what threads to go?