C# SemaphoreSlim

Summary: in this tutorial, you’ll learn how to use the C# SemaphoreSlim to limit the number of threads that can access a shared resource concurrently.

Introduction to the C# SemaphoreSlim class

A semaphore is a mechanism to limit the number of threads that can access a shared resource simultaneously.

The semaphore concept is based on a counter that specifies the number of available resources.

To access the shared resource, a thread needs to request a permit from the semaphore.

If the permit is available, the semaphore will decrement the counter. However, if the counter is zero, the semaphore will block until a permit becomes available.

Once the thread completes processing the shared resource, it needs to release the permit back to the semaphore so that other threads can get the permit. When the thread releases the permit, the semaphore increments the counter

C# has two classes that implement the semaphore concept: Semaphore and SemaphoreSlim.

The Semaphore class has been available since the early version of the .NET Framework. The SemaphoreSlim is the more recent class introduced in .NET Framework 4.0 and .NET core.

The SemaphoreSlim class is a lightweight implementation of the Semaphore class. The SemaphoreSlim is faster and more memory efficient than the Semaphore class.

How to use the SemaphoreSlim class

To use the SemaphoreSlim class, you follow these steps:

First, create a SemaphoreSlim object and pass the initial number of permits to its constructor:

SemaphoreSlim semaphore = new(3);Code language: C# (cs)

In this example, the semaphore object has an initial counter of 3. It means that up to three threads are allowed to access shared resources concurrently.

Second, call the WaitAsync() method of the semaphore object to request a permit:

await semaphore.WaitAsync();Code language: C# (cs)

The WaitAsync() method returns a Task object that waits for the permit to be granted.

Third, call the Release() method of semaphore object to release the permit once you completed accessing the shared resource:

semaphore.Release();Code language: C# (cs)

It’s a good practice to use the try...finally block to ensure that the Release() method is always called even if an exception is raised while accessing the shared resource:

await semaphore.WaitAsync();

try
{
    // Access the shared resource
}
finally
{
    semaphore.Release();
}Code language: C# (cs)

The following example demonstrates how to use the SemaphoreSlim class to allow a limited number of tasks can access the shared resource simultaneously:

using static System.Console; 

SemaphoreSlim semaphore = new(3);
int amount = 0;

async Task AccessAsync(int id)
{
    WriteLine($"Task {id} is waiting to access the amount.");
    await semaphore.WaitAsync();

    try
    {
        WriteLine($"Task {id} is now accessing the amount.");


        // simulate some work
        await Task.Delay(TimeSpan.FromSeconds(1));
        
        // increase the counter
        Interlocked.Increment(ref amount);


        // completed the work
        WriteLine($"Task {id} has completed accessing the amount {amount}");
    }
    finally
    {
        semaphore.Release();
    }
}


// start 10 tasks to access the amount concurrently
var  tasks = new List<Task>();

for (int i = 1; i <= 10; i++)
{
    tasks.Add(AccessAsync(i));
}

// wait for all task to complete
await Task.WhenAll(tasks);

WriteLine("All tasks completed.");
ReadLine();Code language: C# (cs)

How it works.

First, create a SemaphoreSlim object that allows up to three tasks to access the shared resource at the same time:

SemaphoreSlim semaphore = new(3);Code language: C# (cs)

Next, declare the amount variable that acts as a shared resource:

int amount = 0;Code language: C# (cs)

Then, define an asynchronous method AccessAsync that takes an integer parameter id:

async Task AccessAsync(int id)
{
    WriteLine($"Task {id} is waiting to access the amount.");
    await semaphore.WaitAsync();

    try
    {
        WriteLine($"Task {id} is now accessing the amount.");


        // simulate some work
        await Task.Delay(TimeSpan.FromSeconds(1));
        
        // increase the counter
        Interlocked.Increment(ref amount);


        // completed the work
        WriteLine($"Task {id} has completed accessing the amount {amount}");
    }
    finally
    {
        semaphore.Release();
    }
}Code language: C# (cs)

The AccessAsync() method simulates accessing the shared variable amount using the semaphore object.

The AccessAsync() method call WaitAsync() of the semaphore object to wait for a permit available, increments the amount variable using the Increment() method of the Interlocked class, and returns the permit back to the semaphore using the Release() method of the semaphore object.

The AccessAsync() method also writes some messages to the console to indicate the start and end of the task.

After that, create ten tasks that execute the AccessAsync method and wait for them to complete using the Task.WhenAll() method:

// start 10 tasks to access the amount concurrently
var  tasks = new List<Task>();

for (int i = 1; i <= 10; i++)
{
    tasks.Add(AccessAsync(i));
}

// wait for all task to complete
await Task.WhenAll(tasks);Code language: C# (cs)

Finally, write a message to the console to notify the completion of all the tasks and wait for the user to press a key before terminating the program:

WriteLine("All tasks completed.");
ReadLine();Code language: C# (cs)

The following shows the output of the program:

Task 1 is waiting to access the amount.
Task 1 is now accessing the amount.
Task 2 is waiting to access the amount.
Task 2 is now accessing the amount.
Task 3 is waiting to access the amount.
Task 3 is now accessing the amount.
Task 4 is waiting to access the amount.
Task 5 is waiting to access the amount.
Task 6 is waiting to access the amount.
Task 7 is waiting to access the amount.
Task 8 is waiting to access the amount.
Task 9 is waiting to access the amount.
Task 10 is waiting to access the amount.
Task 1 has completed accessing the amount 1
Task 3 has completed accessing the amount 2
Task 5 is now accessing the amount.
Task 4 is now accessing the amount.
Task 2 has completed accessing the amount 3
Task 6 is now accessing the amount.
Task 5 has completed accessing the amount 6
Task 6 has completed accessing the amount 6
Task 8 is now accessing the amount.
Task 7 is now accessing the amount.
Task 4 has completed accessing the amount 6
Task 9 is now accessing the amount.
Task 9 has completed accessing the amount 7
Task 8 has completed accessing the amount 8
Task 7 has completed accessing the amount 9
Task 10 is now accessing the amount.
Task 10 has completed accessing the amount 10
All tasks completed.Code language: C# (cs)

The output shows that:

  • Only three tasks can access the shared variable amount concurrently.
  • Once a permit is available, the next task can access the shared variable.

A practical example of the SemaphoreSlim class

The following program downloads files asynchronously using SemaphoreSlim and HttpClient classes:

using static System.Console;


var client = new HttpClient();
var semaphore = new SemaphoreSlim(3);

async Task DownloadFileAsync(string url)
{
    await semaphore.WaitAsync();

    try
    {
        WriteLine($"Downloading file {url}...");

        var response = await client.GetAsync(url);

        response.EnsureSuccessStatusCode();

        using (var stream = await response.Content.ReadAsStreamAsync())
        using (var fileStream = File.Create(Path.GetFileName(url)))
        {
            await stream.CopyToAsync(fileStream);
        }

        WriteLine($"Completed Downloading file {url}.");
    }
    finally
    {
        semaphore.Release();
    }
}

var urls = new List<string>
{
    "https://www.ietf.org/rfc/rfc791.txt",
    "https://www.ietf.org/rfc/rfc792.txt",
    "https://www.ietf.org/rfc/rfc793.txt",
    "https://www.ietf.org/rfc/rfc794.txt",
    "https://www.ietf.org/rfc/rfc795.txt",
    "https://www.ietf.org/rfc/rfc796.txt",
    "https://www.ietf.org/rfc/rfc797.txt",
    "https://www.ietf.org/rfc/rfc798.txt",
    "https://www.ietf.org/rfc/rfc799.txt",
    "https://www.ietf.org/rfc/rfc800.txt",
};

var tasks = new List<Task>();

foreach (var url in urls)
{
    tasks.Add(DownloadFileAsync(url));
}

await Task.WhenAll(tasks);

WriteLine("Completed downloading all files.");
Code language: C# (cs)

How it works.

First, create a new instance of the HttpClient class that makes HTTP requests:

var client = new HttpClient();
Code language: C# (cs)

Second, create a new SemaphoreSlim object that limits the number of concurrent download to three:

var semaphore = new SemaphoreSlim(3);
Code language: C# (cs)

Third, define the DownloadFileAsync() method that takes a URL as a parameter. The method waits on the semaphore using the WaitAsync() method, use the HttpClient object to download the file specified by the URL, and save the file into the file system. Also, the method releases the semaphore using the Release() method:

async Task DownloadFileAsync(string url)
{
    await semaphore.WaitAsync();

    try
    {
        WriteLine($"Downloading file {url}...");

        var response = await client.GetAsync(url);

        response.EnsureSuccessStatusCode();

        using (var stream = await response.Content.ReadAsStreamAsync())
        using (var fileStream = File.Create(Path.GetFileName(url)))
        {
            await stream.CopyToAsync(fileStream);
        }

        WriteLine($"Completed Downloading file {url}.");
    }
    finally
    {
        semaphore.Release();
    }
}
Code language: JavaScript (javascript)

Fourth, define a list of URLs of the files to download:

var urls = new List<string>
{
    "https://www.ietf.org/rfc/rfc791.txt",
    "https://www.ietf.org/rfc/rfc792.txt",
    "https://www.ietf.org/rfc/rfc793.txt",
    "https://www.ietf.org/rfc/rfc794.txt",
    "https://www.ietf.org/rfc/rfc795.txt",
    "https://www.ietf.org/rfc/rfc796.txt",
    "https://www.ietf.org/rfc/rfc797.txt",
    "https://www.ietf.org/rfc/rfc798.txt",
    "https://www.ietf.org/rfc/rfc799.txt",
    "https://www.ietf.org/rfc/rfc800.txt",
};Code language: C# (cs)

Fifth, create a list of tasks that execute the DownloadFileAsync() concurrently and wait for all the tasks to complete using the Task.WhenAll() method:

var tasks = new List<Task>();

foreach (var url in urls)
{
    tasks.Add(DownloadFileAsync(url));
}

await Task.WhenAll(tasks);Code language: C# (cs)

Finally, output a message to the console to notify that process of downloading completes:

WriteLine("Completed downloading all files.");Code language: C# (cs)

The output shows that there are up to threads that download the files concurrently:

Downloading file https://www.ietf.org/rfc/rfc791.txt...
Downloading file https://www.ietf.org/rfc/rfc792.txt...
Downloading file https://www.ietf.org/rfc/rfc793.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc793.txt.
Downloading file https://www.ietf.org/rfc/rfc794.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc794.txt.
Downloading file https://www.ietf.org/rfc/rfc795.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc792.txt.
Downloading file https://www.ietf.org/rfc/rfc796.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc791.txt.
Downloading file https://www.ietf.org/rfc/rfc797.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc795.txt.
Downloading file https://www.ietf.org/rfc/rfc798.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc796.txt.
Downloading file https://www.ietf.org/rfc/rfc799.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc797.txt.
Downloading file https://www.ietf.org/rfc/rfc800.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc799.txt.
Completed Downloading file https://www.ietf.org/rfc/rfc800.txt.
Completed Downloading file https://www.ietf.org/rfc/rfc798.txt.
Completed downloading all files.Code language: plaintext (plaintext)

Summary

  • Semaphore limits the number of threads that can access shared resources concurrently.
  • Use the C# SemaphoreSlim class to implement the semaphore pattern.
Was this tutorial helpful ?