Synchronization Lock
A synchronization lock is a blocking lock whose only purpose is to park tasks waiting for an event, with no state to provide mutual exclusion on its own. It is the weakest form of blocking lock: its only state is the list of blocked tasks. Because it carries no counter or availability flag, an acquiring task always blocks, and a release with no waiter is lost.
Why pair it with a separate mutex lock?
Because a synchronization lock cannot check conditions itself. You need mutual exclusion outside it to safely examine shared state before deciding to block, and to set shared state after unblocking. The sync lock handles the long-term wait; the mutex lock protects the short-term check. In uC++ this is the
uOwnerLock+uCondLockidiom.
Core properties
- Acquiring task always blocks (no state to make it conditional).
- Release is lost when no task is waiting (no state to remember it).
- Often renamed
wait/signal(ornotify) to match condition-variable terminology.
External locking implementation
The sync lock stores only a list; mutual exclusion comes from an external lock.
class SyncLock {
Task * list;
public:
SyncLock() : list( nullptr ) {}
void acquire() {
// add self to task list
yieldNoSchedule();
}
void release() {
if ( list != nullptr ) {
// remove one task from blocked list, make ready
}
}
};The restaurant analogy
Think of walking into a crowded restaurant. You check the podium (mutex lock) to see if a table is free; the podium itself serializes short-term checks. If no table, you wait on the bench (sync lock) for long-term blocking. When a table frees, the releasing task wakes the next waiter and hands off state.
Canonical usage pattern
MutexLock m;
SyncLock s;
bool occupied = false;
// acquirer
m.acquire();
if ( occupied ) {
s.acquire( m ); // atomically: release m, block on s, re-acquire m
}
occupied = true;
m.release();
// ... use resource ...
// releaser
m.acquire();
occupied = false;
s.release(); // lost if nobody waiting (but that's fine, occupied=false)
m.release();Key trick: s.acquire( m ) takes the mutex as an argument so the runtime can yield-and-release atomically, preventing the classic race between a blocking task and a releasing task.
Internal locking variant
An alternative implementation embeds a spinlock inside the sync lock so it is self-contained but still takes the external mutex to coordinate state.
class SyncLock {
Task * list;
SpinLock lock;
public:
void acquire( MutexLock & m ) {
lock.acquire();
// add self to task list
m.release();
yieldNoSchedule( lock ); // yield, scheduler unlocks lock
m.acquire();
}
void release() {
lock.acquire();
if ( list != nullptr ) { /* unblock one */ }
lock.release();
}
};Barging avoidance vs prevention
When the releaser resets shared state, a newly-arrived task can barge in before the woken waiter runs. Two fixes:
- Avoidance: releaser does not reset state; the woken waiter inherits availability.
- Prevention: releaser holds the mutex lock across the unblock so no third task can race in.