External scheduling is not as powerful as internal scheduling.
Ex) the dating service problem
senario:
1. a girl comes in with ccode
2. if there is no boy with same ccode, she waits in the bench
3. else, she leaves a phone number on the board and wake a boy, and sleeps on a chair
4. the boy wakes up with her phone number.
5. the boy leaves his phone number on another board.
6. the boy wakes the girl and leaves
7. the girl wakes up with his phone number and leaves.
_Monitor DatingService {
enum { CCodes = 20 }; // compatibility codes
uCondition girls[CCodes], boys[CCodes], exchange;
int girlPhoneNo, boyPhoneNo;
public:
int girl( int phoneNo, int ccode ) {
if ( boys[ccode].empty() ) { // no compatible boy ?
girls[ccode].wait(); // wait for boy
girlPhoneNo = phoneNo; // make phone number available
exchange.signal(); // wake boy from chair
} else {
girlPhoneNo = phoneNo; // make phone number available
// boys[ccode].signalBlock() can replace lower 2 lines
boys[ccode].signal(); // wake boy
// you can use girls[ccode] as a chair instead of
// 'exchange' since its empty banch.
exchange.wait(); // sit in chair
}
return boyPhoneNo;
}
int boy( int phoneNo, int ccode ) {
// same as above, with boy/girl interchanged
}
};
Solution 3 of previous reader/writers problem.
No barging. solves all 5 rules.
BUT with temperal barging.
_Monitor ReadersWriter {
int rcnt = 0, wcnt = 0;
// the moment we seperate readers, writers, it has temperal barging.
uCondition readers, writers;
public:
void startRead() {
if ( wcnt != 0 | | ! writers.empty() ) readers.wait();
rcnt += 1;
readers.signal();
}
void endRead() {
rcnt -= 1;
if ( rcnt == 0 ) writers.signal();
}
void startWrite() {
if ( wcnt !=0 | | rcnt != 0 ) writers.wait();
wcnt = 1;
}
void endWrite() {
wcnt = 0;
if ( ! readers.empty() ) readers.signal();
else writers.signal();
}
};
But this only allows one reader at a time.
Ex) conversion to 1 step protocal + allowing multiple reads at a time
_Monitor ReadersWriter {
_Mutex void startRead() {
if ( ! writers.empty() ) readers.wait();
rcnt += 1;
readers.signal();
}
_Mutex void endRead() { . . . }
public:
_Nomutex void read(. . .) { // no const or mutable
startRead(); // acquire mutual exclusion
// read, no mutual exclusion
endRead(); // release mutual exclusion
}
void write(. . .) { // acquire mutual exclusion
if ( rcnt != 0 ) writers.wait(); // release/reacquire
// write, mutual exclusion
if ( ! readers.empty() ) readers.signal();
else writers.signal();
}
};
back to 2 step protocol + solving rule 6 - fixing temperal barging
_Monitor ReadersWriter {
int rcnt = 0, wcnt = 0;
uCondition RWers;
enum RW { READER, WRITER };
public:
void startRead() {
if ( wcnt !=0 | | ! RWers.empty() ) RWers.wait( READER );
rcnt += 1;
if ( ! RWers.empty() && RWers.front() == READER ) RWers.signal();
}
void endRead() {
rcnt -= 1;
if ( rcnt == 0 ) RWers.signal();
}
void startWrite() {
if ( wcnt != 0 | | rcnt != 0 ) RWers.wait( WRITER );
wcnt = 1;
}
void endWrite() {
wcnt = 0;
RWers.signal();
}
};
passing parameter to uCondition::wait() attaches the information on the shadow queue of the internal condition lock.
you can ask only one question: uCondition::front() - what is the tag on the first item of the shadow queue.
readers and writers problem solution 8 with external scheduling
Monitor ReadersWriter {
int rcnt = 0, wcnt = 0;
public:
void endRead() {
rcnt -= 1;
}
void endWrite() {
wcnt = 0;
}
void startRead() {
if ( wcnt > 0 ) _Accept( endWrite );
rcnt += 1;
}
void startWrite() {
if ( wcnt > 0 ) _Accept( endWrite );
else while ( rcnt > 0 ) _Accept( endRead );
wcnt = 1;
}
};
This does not cause startWrite() to starve by startRead().
That is because when _Accept(endRead) is called, it does not just leave but it activly hands endRead() a botton; so startWrite will call as much endRead as rcnt and while doing so, no startRead can barge in.
call to _Accept(...) may not return with proper results.
it may be the case where the method in _Accept throws an exception.
in such case, _Accept throws an exception called uMutexFailure::RendezvousFailure
_Monitor M {
public:
void mem1() {
. . . if ( . . . ) _Throw E(); . . . // E goes to caller
} // uRendezvousFailure goes to “this”
void mem2() {
try {
. . . if ( . . . ) _Accept( mem1 ); . . .
} catch( uMutexFailure::RendezvousFailure & ) { // implicitly enabled
// deal with rendezvous failure
} // try
}
};
There are 3 or 4 exception in uC++ that is SO important that it is always turned on (regardless of _Enable), and uMutexFailure::RendezvousFailure
is one of them.
_Accept call with multiple member routines - _Accept(mem1, mem2, ...) - may require flag variables to make distinguish between which member that has failed.
If only way to get to M2
is through M1
, this may result in a deadlock.
Same problems can happen with any kinds of lock and it is called lock composition problem.