The following code is correct, but the thread grabs the lock almost all the time, which is unfair:
right_time = false;
while (!right_time) {
mutex_lock(&lock);
// condition here happens once per year
if (condition) {
right_time = true;
} else {
mutex_unlock(&lock);
// we can't add sleep() or yeield() here
// because we can't predict exactly how long to sleep
}
}
// do something at right time
mutex_unlock(&lock);
Condition Variable: We solve above issue by letting other thread tell us when to start running
cond_wait(cvar, world_lock) {
lock(cvar->lock);
enq(cvar->queue, my_thread_id());
unlock(world_lock);
ATOMICALLY {
// to implement "ATOMICALLY":
// we could disable interrupts
// we could rely on OS (bad)
// we could have a better thread-block interface (best)
unlock(cvar->lock);
thread_block(); // kernel please sleep me
// we hope someone grab the cvar->lock and wake me up (not before I sleep)
}
lock(world_lock);
}
mutex_lock(&lock);
while (cvar->p = get_time_to_wait()) {
cond_wait(cvar, &lock);
}
// do something at right time
mutex_unlock(&lock);
The other thread does
cond_broadcast(time_to_wait);
cond_wait()andcond_signal()can be called at the same time.
Conditional Variable Correctness:
when stop, a thread "basically" does not run
when wake, a thread "probably" can produce work
when multiple thread wake, "substantial fraction" of them can produce work
If semaphore is only allows 0 and 1, then it's a mutex lock:
semaphore m = 1; // number of available critical sections
do {
P(m); // wait
// critical section
V(m); // signal
// non-critical section
} while (true);
There are all kinds of semaphore:
binary, counting, etc.
non-blocking
deadlock-avoidance
The implementation of wait() is similar to cond_wait().
wait(semaphore s) {
lock(s->lock);
--s->count;
if (s->count < 0) {
enq(s->queue, my_thread_id());
ATOMICALLY {
unlock(s->lock);
thread_block();
}
} else {
unlock(s->lock);
}
}
signal(semaphore s) {
lock(s->lock);
++s->count;
if (s->count <= 0) {
// wake up one thread
int tid = deq(s->queue);
thread_unblock(tid);
}
unlock(s->lock);
}
lock() and unlock() can be implemented using mutex or OS-assisted atomic de-scheduling / awakening.
Some compiler adds synchronization code. To achieve: threads running in any procedure blocks all thread entires.
So imagine
{and}are likelock()andunlock(), but they are invisible to programmers.
For waiting, programmers do this: (and is implemented via conditional variable)
// say someone tries to withdraw money before deposit, we want withdraw to wait
// deposit() will call cond_broadcast() when done
// This code is actually wrong
void stubbornly_cash_check(acc a, check c) {
while(account[a].balance < check.val) {
cond_wait(account[a].activity);
}
account[a].bal -= check.val;
}
What if you have signal() inside monitor block {}? No. We see signal() as a special kind of return. So signal() must be the last statement in the monitor block. (otherwise we have to deal with the case that the thread is woken up but the monitor is still locked)
More about async safety
Table of Content