Locking patterns are used to handle complicated critical sections.
Split binary semaphore
have group of semaphores
treat them as if they are single binary semaphore
i.e. sum of all semaphores are either 0 or 1.
Used when different kinds of tasks have to block separately.
Baton passing
a thread wakes up with a lock locked.
baton is conceptually accuired but doesn't really exists.
Used to force some thread to execute without barging.
class BinSem {
queue<Task> blocked;
bool avail;
SpinLock lock;
public:
BinSem( bool start = true ) : avail( start ) {}
void P() {
lock.acquire(); PICKUP BATON, CAN ACCESS STATE
if ( ! avail ) {
// add self to lock’s blocked list
PUT DOWN BATON, CANNOT ACCESS STATE
yieldNoSchedule( lock );
// UNBLOCK WITH SPIN LOCK ACQUIRED
PASSED BATON, CAN ACCESS STATE
}
avail = false;
lock.release(); PUT DOWN BATON, CANNOT ACCESS STATE
}
void V() {
lock.acquire(); PICKUP BATON, CAN ACCESS STATE
if ( ! blocked.empty() ) {
// remove task from blocked list and make ready
PASS BATON, CANNOT ACCESS STATE
} else {
avail = true;
lock.release(); PUT DOWN BATON, CANNOT ACCESS STATE
}
}
};
Program reads more than write and reading does not conflict with each other.
You want to allow multiple readers to have simultaneous access,
but serialize access for writer tasks.
Solution 1
There are 3 semaphores
one for each of the lines: readers, writers, entry
There are 4 counters: counters for readers and writers in wait, and in use.
uSemaphore entry(1), rwait(0), wwait(0); // split binary semaphores
int rdel = 0, wdel = 0, rcnt = 0, wcnt = 0; // auxiliary counters
void Reader::main() {
entry.P(); // pickup baton
if ( wcnt > 0 ) { // occupied ?
rdel += 1; entry.V(); // put baton down
rwait.P(); rdel -= 1; // passed baton
}
rcnt += 1;
if ( rdel > 0 ) { // waiting readers ?
rwait.V(); // pass baton
} else {
entry.V(); // put baton down
}
// READ
entry.P(); // pickup baton
rcnt -= 1;
if ( rcnt == 0 && wdel > 0 ) { // waiting writers ?
wwait.V(); // pass baton
} else {
entry.V(); // put baton down
}
}
void Writer::main() {
entry.P(); // pickup baton
if ( rcnt > 0 | | wcnt > 0 ) { // occupied ?
wdel += 1; entry.V(); // put baton down
wwait.P(); wdel -= 1; // passed baton
}
wcnt += 1;
entry.V(); // put baton down
// WRITE
entry.P(); // pickup baton
wcnt -= 1;
if ( rdel > 0 ) { // waiting readers ?
rwait.V(); // pass baton
} else if ( wdel > 0 ) { // waiting writers ?
wwait.V(); // pass baton
} else {
entry.V(); // put baton down
}
}
Notice that a semaphore must be Ved in order to P on a nother semaphore.
This makes only one semaphore alive/in-use at any moment.
This solution has one problem: writer's starvation
Solution:
on reader's side, on acquiring the entry semaphore,
Also wait if there is a waiting write.
// reader
entry.P();
if ( wcnt > 0 | | wdel > 0 ) {
...
But this solution has a temporal barging
The problem: 2:00 reader should have read 1:30 writer's value
This is called staleness/freshness
To resolve the staleness issue, read and write queue into a single queue.
But to do this, we need some way of telling the kind of the task that will be woken up next.
Solution 1: use a shadow queue to keep track of arriving tasks instead of having waiting counters.
solution 2: introduce a special chair for writer. Reader wakes up next line in lock. if the woken task is a reader, repeat; else, put woken writer on the chair. any task that exits critical section must check the chair first.
But there exists a case where schedules are out of order when you V on entry semaphore than P for waiting semaphore.
To resolve this, you need some way of automically V and P on semaphores.
uC++ has this:
semaA.P(semaB); // P on semaA while V on semaB