why do we have different types of locks?
Spinning Locks
Blocking Locks
Blocking locks reduce busy waiting by having releasing task do additional work: coopertion
divided into two kinds
Implementation - two for one lock!
class MutexLock{
bool avail;
Task * owner
queue <Task> blocked;
SpinLock lock;
public:
MutexLock() : avail(true), owner(nullptr){}
void acquire(){
lock.acquire(); // barging. potential starvation
while(!avail && owner!=currThread()){ // busy wait
// add self to lock's blocked list
// <<< lock.release(); releasing here does not work
// because at this right moment, there may be release called
// and tries to wake you, but you haven't gone sleep yet
// so it passes by and then you go to sleep.
// and no one wakes you up ever again.
yieldNoSchedule(lock); // do not reschedule to ready queue
// lock is passed to yieldNoSchedule and the KERNAL releases
// the lock for you after you go to sleep.
// normal yield puts itself in to a ready queue
lock.acqcuire(); // reacquire spinlock
// ^^ this lock.acquire can compete with the first acquire
// which caues a barging.
}
avail = false;
owner = currThread();
lock.release();
}
void release(){
lock.acquire();
if(owner!=currThread()) ... // ERROR CHECK
owner = nullptr
if(!blocked.empth()){
// remove task from blocked list and make ready
}
avail = true; // reset
lock.release(); // RACE
}
}
Barging avoidance.
you allow ppl to barge. but, as soon as someone barges, you redirect him to a chair!
if there is a person in a blocked list, no new commers can go passed the door!
void acquire(){
lock.acquire();
if(!avail && owner != currThread()){ // avoid barging
// add self to lock's blocked list
yieldNoSchedule(lock);
lock.acquire();
}
avail = false;
owner = currThread();
lock.release();
}
void release(){
lock.acquire();
owner = nullptr;
if(!blocked.empty()){
// remove task from blocked list and make ready
}else{
avail = true;
}
lock.release();
}
Barging prevension
void acquire(){
lock.acquire();
if (!avail && owner != currThread()){
// add self to lock's blocked list
yieldNoSchedule(lock);
// DO NOT REACQUIRE LOCK!!!
}
avail = false;
owner = currThread();
lock.release();
}
void release(){
lock.acquire();
owner = nullptr;
if(!blocked.empty()){
//remove task from blocked list and make ready
// DO NOT RELEASE LOCK
}else{
avail = true;
lock.release();
}
}
here, in some case, release() acquires the spin lock and acquire() releases the spin lock.
Is there a reason you would perfer barging avoidence rather than barging prevension?
A: If you are not in a position that you can seperate conceptual brackets of lock acquire and lock release - baton passing - you cannot use barging prevension.
Blocking multi-acquisition mutex-lockin uC++ requiring multiple releases.
class uOwnerLock{
public:
uOwnerLock();
uBaseTask * owner();
unsigned int times();
void acquire();
bool tryacquire();
void release();
}
owner() returns the address of task which can be compared with uThisTsk().
uOwnerLock lock;
lock.acquire();
try{
...
}_Finally{
lock.release();
}
** this may not work depending on what lock you are using because you may have to release it multiple times.
This prevents potential deadlock caused by exceptions.
in regular c++, use RAII - Resource Acquisition Is Initialization.
class RAII{
uOwnerLock &lock;
public:
RAII(uOwnerLock &lock): lock(lock) {lock.acquire();}
~RAII(){lock.release();}
};
uOwnerLock lock;
{
RAII raii(lock);
...
}
RAII object will call its destructor upon exception, or exiting scope.
IO stream is a CS.
uC++ has a built in mutex lock for IO based on uOwnerLock
task1: osacquire(cout) << "abc" << "def" << endl;
task2: osacquire(cout) << "uvw" << "xyz" << endl;
the ending semicolon releases the object.
acquire IO stream for block of instructions
{
osacquire OA(cout);
cout << ... << endl;
cout << ... << endl;
// you can acquire it multiple times because it has ownerships
// it wouldn't be any different from just using cout in this context
osacquire(cout) << ... << endl;
} // OA is destructed here, so lock is released.