Technique Implement a Lock that will allow Multiple readers to access a certain memory value, or a single writer to modify a certain memory value.
Motivation If there are multiple readers, the data should not be able to be modified by a writer. But, by allowing multiple readers it is possible to do more work instead of just blocking any other readers. Allowing multiple writers leads to unpredicted behavior, as well as having a reader and writer active at the same time. As a result, there is no need to block if there are either multiple readers or one writer.
Implementation It is necessary to maintain a READ COUNT variable that is protected by a mutex (semaphore)
Write_lock ()
{
P(m) // lock
while ( NumReaders > 0 )
{
V(m) // unlock
P(m) // lock
}
}
But wait - we are polling which is bad!!!
Solution - We can use a Write Exclusion Semaphore which allows writers to block until there are no more active readers.
Symbol Table
| M | mutex |
| NR | read count |
| WX | write exclusion |
Read_lock ()
{
P(M)
if (++NR == 1)
P(WX)
V(M)
}
Read_unlock()
{
P(M)
if (--NR == 0)
V(WX)
V(M)
}
Write_unlock ()
{
V(WX)
}
Write_lock ()
{
P(WX)
}
Note that this implementation priviledges readers; if there is one writer and a thousands of readers, the writer will not get to have the lock.
| Proccess1 | Process 2 | Mutex | ReadCount | Write Exclusion |
|---|---|---|---|---|
| initial status | initial status | 1 | 0 | 1 |
| read_lock | 1 | 1 | 0 | |
| /*executing | write_lock | 1 | 1 | 0 |
| read_unlock | /*blocked*/ | 1 | 0 | 1 |
| /*w_lock unblocks*/ | 1 | 0 | 0 | |
| read_lock | /*executing*/ | 0 | 1 | 0 |
| /*blocked*/ | /*executing*/ | 0 | 1 | 0 |
| /*blocked*/ | write_unlock | 0 | 1 | 1 |
| /*r_lock unblocked*/ | 1 | 1 | 0 | |
| /*executing*/ | 1 | 1 | 0 | |
| read_unlock | 1 | 0 | 1 |
Note that in the table, the write_lock operation are waiting for all read operations to finish. This indicates a preference towards readers.
| M | mutex |
| NR | read count |
| WX | write exclusion |
| M2 | mutex |
| NW | writer count |
| RX | read exclusion |
Note: new code is commented.
Read_lock()
{
P(RX) //new
P(M)
if (++NR == 1)
P(WX)
V(M)
V(RX) //new
}
Read_unlock()
{
P(M)
if (--NR == 0)
V(WX)
V(M)
}
Write_unlock()
{
V(WX)
P(M2) //new
if (--NW == 0) //new
V(RX) //new
V(M2) //new
}
Write_lock()
{
P(M2) //new
if (++NW ==1) //new
P(RX) //new
V(M2) //new
P(WX)
}
Explanation
The behavior here is that reader exclusion allows multiple readers and write exclusion allows one write at a time. This is seen in the placement of both P(RX) and V(RX) in the read lock code, and staggering the P(WX) and V(WX) across the write lock and unlock code.
| Action | M | NR | WX | M2 | NW | RX |
|---|---|---|---|---|---|---|
| Initial values | 1 | 0 | 1 | 1 | 0 | 1 |
| P1 readlock | 1 | 1 | 0 | 1 | 0 | 1 |
| P2 writelock | 1 | 1 | 0 | 1 | 1 | 0 |
| P3 readlock | 1 | 1 | 0 | 1 | 1 | 0 |
| P1 readunlock | 1 | 1 | 1 | 1 | 1 | 0 |
| unblock P2 | 1 | 0 | 0 | 1 | 1 | 0 |
| P2 writeunlock | 1 | 0 | 1 | 1 | 0 | 1 |
| unblock P3 | 1 | 0 | 0 | 1 | 0 | 1 |
What is it? A condition variable allows a process to block until a specified condition is fulfilled. This condition is often represented as Boolean expression.
Examples of conditions – hell freezes over, some integer i < 100, etc.
Condition variables turn this expression into an object.
This requires multiple semaphores as well as a waitcounter. We can't use a single semaphore because wait always blocks unlike P(), and signal does not behave like V().
| Initial Values | ||
|---|---|---|
| M | mutex | 1 |
| W | number of waiters | 0 |
| E | event semaphore | 0 |
Events have 2 operations: wait() and signal().
wait(E)
// blocks until a signal happens or is raised
{
P(M);
++NW;
V(M);
P(E);
}
signal(E)
//wake up 1 waiter, if there are any
//otherwise, nothing happens (signal goes away)
{
P(M)
if ( NW > 0 )
{
NW--
V(E)
}
V(M)
}
process 1
... /*do some stuff*/ wait(Hell_is_frozen); /* Brrrr, Satan needs a sweater!*/ ...
process 2
... freeze (Hell); signal(Hell_is_frozen); ...
Note that it is the application's responsibility to raise a signal when a condition is met.
Avoid race conditions by adding mutex operations atomically.
Wait(CV, M)
{
//these next two operations are now atomic
Unlock(M) //usually called this way because we have Mutex at this time
Event.wait (CV)
Lock (M)
}
Signal(CV)
{
Event signal(CV)
}
Broadcast(CV) //like signal, but wakes up __all__ threads waiting on CV
For implementation of the above, look at pthead_cond_var_t and pthread_mutext_t objects.
Barriers are a type of synchronization method that blocks threads until a specified number of threads have been blocked, and then unlocks all the threads.
Motivation example – Matrix Math
(M1* M2) + (M3*M4) + (M5*M6)
Thread 1 mutiplies M1 with M2, then blocks.
Thread 2 multiplies M3 with M4, then blocks.
Thread 3 multiplies M5 with M6, then blocks.
After all three threads are blocked, the barrier unblocks them, and the result can be added.
Barrier(B) there are N threads. Block until all N threads are blocked in barrier B, then unblock all threads.
In this section, we will demonstrate the use of different sychrnization techniques with a Queue with a Max Capacity. For Reference, the handout given out in class is available here.
Queue Operations
enq(Q,E) // if there is space fore E, enqueue it on Q
E = deq(Q) /*nonblocking method*/ ////////////////////// //dequeue from Q and return first element of Queue //if Q is empty, fail /*blocking method*/ /////////////////// //block until the queue is not empty //then dqueue and return the first element
Labeling Critical Sections
Why? If we have at most one dequeuer and one enqueuer, the enqueuer will always access the head of the queue, and the dequeuer will always access and modify the tail of the queue. If we limit ourselves to 1 active dequeuer and enqueuer, the only case that can be troublesome is if the queue is empty. As a result, we can implement a lock-free queue and bypass the overhead costs of locking and unlocking.
Idea If there is at most one reading thread and at most one writing thread, it is safe to avoid locks if each variable is written by one thread.
Implementation Given a size, head, and tail pointers, the head is only written by deq(...), the tail is only written by enq(...). In this way, there should not be interference amongst the threads because pointers accessible by running threads are mutually exclusive.
--- Jay Mencio 2006/05/08 21:59
--- Julia Uskolovsky 2006/05/08 22:01