Assume you own a bar that have single restroom with n stalls. In order to avoid lawsuits you want to make sure that only people of the same gender can be in the restroom at the same time and no accidents occur (nobody peed their pants). How would you write synchronization algorithm for it?
Translating into concurrency language we want to build a synchronization algorithm that allow no more than n threads of the same kind to enter critical section and it should be starvation free (threads that are trying to enter critical section eventually enter it).
The problem can be divided three parts:
- How to allow only threads of the same kind to enter critical section
- How to limit number of threads inside critical section to configured number
- How to make the whole thing starvation free
Mutual exclusion algorithms have a good property with respect to starvation freedom. Assume you have two starvation free mutual algorithms A and B. Combined in the following way:
Enter code A
Enter code B
Critical section
Leave code B
Leave Code A
they form another starvation free mutual exclusion algorithm.
Limiting number of threads inside critical section to configured number can be easily solved with SemaphoreSlim (starvation free). Thus we need to solve problem of allowing only threads of the same kind to enter critical section.
Let’s denote two types of threads: black and white. Assume that thread tries to enter critical section. The following case are possible:
- No other threads are in the critical section, so it can enter
- There are threads of the same color in the critical section
- no threads of the different color are waiting, so it can enter
- there are waiting threads of the different color, so it cannot enter to prevent starvation of the waiting threads and must wait
- There are threads of the different color in the critical section so it must wait
Now to prevent starvation we will switch turn to the different color once group of threads of current color leaves critical section. Turn is set to color that is now allowed to enter critical section. However if no threads are in the critical section a thread may enter if its not its turn (basically it captures the turn).
class WhiteBlackLock { private readonly object _sync = new object(); private readonly int[] _waiting = new int[2]; private int _turn; private int _count; public IDisposable EnterWhite() { return Enter(0); } public IDisposable EnterBlack() { return Enter(1); } private IDisposable Enter(int color) { lock (_sync) { if (_waiting[1 - _turn] == 0 && (_count == 0 || _turn == color)) { // Nobody is waiting and either no one is in the // critical section or this thread has the same // color _count++; _turn = color; } else { // Either somebody is waiting to enter critical // section or this thread has a different color // than the ones already in the critical section // and thus wait with the rest of the same color _waiting[color]++; // Wait until current group while (_waiting[color] > 0) Monitor.Wait(_sync); } // Wrap critical section leaving in a disposable to // enable convenient use with using statement return new Disposable(this); } } private void Leave() { lock (_sync) { // Indicate completion if (--_count != 0) return; // If this is the last one of the current group make // way for threads of other color to run by switching // turn _turn = 1 - _turn; // Before threads are awoken count must be set to // waiting group size so that they can properly report // their completion and not change turn too fast _count = _waiting[_turn]; // Indicatet that current group can enter critical // section _waiting[_turn] = 0; // Wake up wating threads Monitor.PulseAll(_sync); } } class Disposable : IDisposable { private readonly WhiteBlackLock _lock; private int _disposed; public Disposable(WhiteBlackLock @lock) { _lock = @lock; } public void Dispose() { // Make sure only the first call of allowed multiple // calls leaves critical section if (Interlocked.Exchange(ref _disposed, 1) == 0) _lock.Leave(); } } }
In order to avoid lock ownership tracking leaving critical section is represented through disposable.
var semaphore = new SemaphoreSlim(n); var whiteBlackLock = new WhiteBlackLock(); // Worker thread code to enter critical section using (whiteBlackLock.EnterWhite()) { semaphore.Wait(); // Critical section goes here semaphore.Release(); }
Now your restroom can safely serve the needs of your customers =).
5 comments:
Your restroom synchronization problem seems to assume that threads cannot be killed while waiting. In particular: what happens if a thread is killed while blocking on "Monitor.Wait(_sync);" ?
The "_waiting[color]++;" is not undone so this leads to a miscount later, e.g. _count never reaching 0.
Further, what if men and women are waiting for women to finish, and all the men die. Should not the queued women be able to proceed?
Actually this post is about synchronization algorithm rather than reliability which is a separate and tricky area.
However I think releasing lock automatically on behalf of dead or killed thread is a dangerous practice as you do no not know in what state protected by lock invariants are left. You can read more on a similar problem in Locks and exceptions do not mix post from Eric Lippert.
Fair enough. It's now documented that there are no asynchronous exceptions, the Enter constructors cannot fail, and the critical section code always continues normally.
I only noticed the problem while putting together a generalized solution in another language for fun. Hopefully it will be exception safe.
Hi Dzmitry,
big fan of your blog..I particularly enjoy the posts on concurrency.
Do you mind reviewing my solution to the shared restroom problem and sharing your thoughts?
Post a Comment