Basically a Semaphore allows us to set the initial count and the maximum number of threads than can enter a critical section.
The standard analogy of how a Semaphore works is the nightclub and bouncer analogy. So a nightclub has a capacity of X and the bouncer stops any clubbers entering once the nightclub reaches it’s capacity. Those clubbers can only enter when somebody leaves the nightclub.
In the case of a Semaphore we use WaitOne (or Wait on SemaphoreSlim) to act as the “bouncer” to the critical section and Release to inform the “bouncer” that a thread has left the critical section. For example
private readonly Semaphore semaphore = new Semaphore(3, 3); private void MyCriticalSection(object o) { sempahore.WaitOne(); // do something semaphore.Release(); }
In the code above, say our calling method creates 20 threads all running the MyCriticalSection code, i.e.
for (int i = 0; i < 20; i++) { Thread thread = new Thread(Run); thread.Start(); }
What happens is that the first three threads to arrive at semaphore.WaitOne will be allowed access to the code between the WaitOne and Release. All other threads block until one or more threads calls release. Thus ensuring that at any one time a maximum of (in this case) 3 threads can have access to the critical section.
Okay but the Semaphore allows an initial count (the first argument) so what’s that about ?
So let’s assume instead we have an initial count of 0, what happens then ?
private readonly Semaphore semaphore = new Semaphore(0, 3);
The above code still says we have a maximum of 3 threads allowed in our critical section, but it’s basically reserved 3 threads (maximum threads – initial count = reserved). In essence this is like saying the calling thread called WaitOne three times. The point being that when we fire off our 20 threads none will gain access to the critical section as all slots are reserved. So we would need to Release some slots before the blocked threads would be allowed into the critical section.
Obviously this is useful if we wanted to start a bunch of threads but we weren’t ready for the critical section to be entered yet.
However we can also set the initial count to another number, so let’s say we set it to 1 and maximum is still 3, now we have a capacity of 3 threads for our critical section but currently only one is allowed to enter the section until the reserved slots are released.
Note: It’s important to be sure that you only release the same number of times that you WaitOne or in the case of reserved slots you can only release up to the number of reserved slots.
To put it more simply, think reference counting. If you WaitOne you must call Release once and only once for each WaitOne. In the case of where we reserved 3 slots you can call Release(3) (or release less than 3) but you cannot release 4 as this would cause a SemaphoreFullException.
Important: Unlike a Mutex or Monitor/lock a Semaphore does not have thread affinity, in other words we can call Release from any thread, not just the thread which called WaitOne.
SemaphoreSlim
SemaphoreSlim, as the name suggests in a lightweight implementation of a Semaphore. The first thing to notice is that it uses Wait instead of WaitOne. The real purpose of the SemaphoreSlim is to supply a faster Semaphore (typically a Semaphore might take 1 ms per WaitOne and per Release, the SemaphoreSlim takes a quarter of this time, source ).
See also
Semaphore and SemaphoreSlim
Overview of Synchronization Primitives
and the brilliant Threading in C#