CodeGym /Courses /C# SELF /Semaphores: Semaphore

Semaphores: Semaphore and SemaphoreSlim

C# SELF
Level 56 , Lesson 3
Available

1. Introduction

Mutex and lock are like a barista who serves one customer at a time. But what if we don't have just one coffee machine, but three — and three cups of coffee can be made at the same time?

For example, you run a cafe with three espresso machines. Customers (threads) come, take a free machine, make coffee and leave. If all three machines are busy, the rest wait until one frees up.

Question: How do you ensure that no more than three customers are using the machines at the same time, and the others wait their turn?
Answer: use a semaphore!

What is a semaphore?

A semaphore is a classic synchronization tool. If lock/Mutex enforce "one in — others wait", a semaphore says: "I allow N at the same time!".

Semaphores were introduced by Edsger Dijkstra in 1965. The name comes from nautical signaling: just like flags transmitted available information, a semaphore in code tells threads — you can enter or you have to wait.

Use cases

  • Limit the number of threads working with a resource simultaneously.
  • Limit concurrent DB connections, parallel requests, heavy tasks.

2. Overview of classes: Semaphore and SemaphoreSlim

Semaphore

  • Heavyweight class, uses OS kernel objects.
  • Supports synchronization between threads of different processes.
  • You can give it a name and share it between processes.

SemaphoreSlim

  • Lightweight version, works only within a single process.
  • Faster and more economical in resources.
  • Almost always preferable when interprocess synchronization is not needed.

Analogy: a daypack (SemaphoreSlim) vs a big suitcase (Semaphore). Traveling light — take the daypack.

Comparison table

Class Interprocess Performance Recommended
Semaphore
Yes Slower When you need synchronization between processes
SemaphoreSlim
No Faster In 99% of cases, within a single process

Main methods and properties of a semaphore

Main parameters

  • InitialCount — the initial number of permits.
  • MaxCount — the maximum number of permits that can be issued simultaneously.

Key methods

  • Wait() or WaitAsync() — request access (take a permit).
  • Release() — release a permit.

How it works
If there are no permits when Wait() is called, the thread blocks and waits until someone calls Release(). After a release, one of the waiting threads continues.

3. First practical example

Let's add a "parking lot" with 3 spots to a console app and try to start 10 threads.

using System;
using System.Threading;

class Program
{
    // Semaphore with 3 permits (3 parking spots)
    static SemaphoreSlim parking = new SemaphoreSlim(3);

    static void Main()
    {
        for (int i = 1; i <= 10; i++)
        {
            int carNumber = i;
            new Thread(() =>
            {
                Console.WriteLine($"Car #{carNumber} is trying to park...");
                parking.Wait(); // Waits for a free spot
                Console.WriteLine($"Car #{carNumber} parked!");
                Thread.Sleep(2000); // Parked for 2 seconds
                Console.WriteLine($"Car #{carNumber} is leaving the parking.");
                parking.Release(); // Free the spot
            }).Start();
        }
    }
}
  • Only three cars will "park" at the same time.
  • The rest will wait for a spot to free up.
  • The output is interleaved — that's normal for multithreading.

4. Semaphore as a load limiter

Let's limit the number of heavy tasks (e.g., downloads) running at the same time to 5.

static SemaphoreSlim semaphore = new SemaphoreSlim(5); // max 5 concurrent downloads

static void DownloadFile(int fileId)
{
    semaphore.Wait();
    try
    {
        Console.WriteLine($"--> Starting download of file {fileId}");
        Thread.Sleep(1000 + fileId * 100); // Downloading (simulation)
        Console.WriteLine($"<-- File {fileId} downloaded");
    }
    finally
    {
        semaphore.Release();
    }
}

static void Main()
{
    for (int i = 1; i <= 12; i++)
    {
        int localId = i;
        new Thread(() => DownloadFile(localId)).Start();
    }
}

Important point: put Wait() before the try block, and Release() in finally. That way the permit is definitely released even if an exception occurs.

5. Wait(int millisecondsTimeout) and async methods

You can wait only for a limited time:

if (semaphore.Wait(500))
{
    // Managed to take a permit within half a second!
}
else
{
    // Didn't get it within 500 ms — timed out
}

In modern apps (for example, ASP.NET) use the asynchronous variant: await semaphore.WaitAsync(). This doesn't block the executing thread while waiting for a permit.
Note: in async code use SemaphoreSlim and its WaitAsync, otherwise you may get unexpected deadlocks.

6. Examples of incorrect and correct usage

A common mistake is forgetting to call Release(): permits "leak" and everything stops.

Bad

static void SomeWork()
{
    semaphore.Wait();
    // ... processing, but Release was forgotten!
}

Good

static void SomeWork()
{
    semaphore.Wait();
    try
    {
        // processing
    }
    finally
    {
        semaphore.Release();
    }
}

Async variant

static async Task SomeAsyncWork()
{
    await semaphore.WaitAsync();
    try
    {
        // asynchronous processing
    }
    finally
    {
        semaphore.Release();
    }
}

7. Internal structure of a semaphore (explained simply)

A semaphore is a counter. Wait() decreases it by 1. If it was > 0 — the thread proceeds; if 0 — the thread waits. Release() increases the counter and wakes waiting threads.


+-------------------------------+
| Semaphore (counter = 3)       |
+-------------------------------+
|  [ ]  [ ]  [ ]                | <--- Permits
+----+----+----+----------------+
     |    |    |
   Thread Thread Thread

8. Useful nuances

Difference from other primitives

  • lock / Monitor / Mutex — allow only one thread (exclusive access).
  • Semaphore/SemaphoreSlim — allow up to N threads concurrently.

A semaphore is not bound to an "owner": any thread can release a permit. That's a feature, not a bug.

Real-world applications

  • Limit parallel connections to a service or DB.
  • Pool: no more than N threads per resource.
  • Limit the number of concurrently processed web requests.
  • Rate read/write to protect from overload.
  • Limit calls to an external API.

Example of error (Release more than Wait)

var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Error! Counter becomes 3, exceeding MaxCount — SemaphoreFullException will be thrown.

This will throw SemaphoreFullException: the counter exceeded the maximum.

Differences between Semaphore and SemaphoreSlim

  • SemaphoreSlim — in-process, faster and simpler (use almost always).
  • Semaphore — needed for interprocess synchronization (rare scenario).

Why know about semaphores?

A classic interview question: "How do you limit the number of threads working with a resource?" — the correct answer: a semaphore.

  • lock — 1 thread.
  • Semaphore/SemaphoreSlimN threads.

9. Common mistakes and usage caveats of semaphores

Mistake #1: forgetting to call Release(). If a thread took a permit (Wait() or WaitAsync()) but didn't release it, others will wait forever — the application will "freeze".

Mistake #2: calling Release() more times than Wait(). Extra permits appear. For Semaphore this will lead to SemaphoreFullException and broken access logic.

Mistake #3: mixing different synchronization mechanisms. Using lock in one place and a semaphore on the same resource elsewhere increases the risk of deadlocks.

Mistake #4: using Semaphore in async code. The classic semaphore doesn't play well with async/await. For async scenarios use SemaphoreSlim and WaitAsync().

Mistake #5: incorrect initialCount and maxCount values. If chosen incorrectly, the limit can be bypassed and more threads than intended may access the resource.

2
Task
C# SELF, level 56, lesson 3
Locked
Create a console application that simulates a parking lot with 2 available parking spots.
Create a console application that simulates a parking lot with 2 available parking spots.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION