The SemaphoreSlim Class: Multitask-Based Programming in C#
2023-06-11
Overview
The SemaphoreSlim class is a structure used in C# to control one or more threads using a specific resource or operation concurrently. SemaphoreSlim limits the number of threads that can access the resource at the same time. The use of SemaphoreSlim is often used to prevent deadlock situations in multi-threaded applications and to ensure that a specific resource is used by only one or more threads at the same time.
Using SemaphoreSlim
The SemaphoreSlim class is used as follows:
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
The first parameter here determines how many threads can use the resource at the same time. The second parameter determines the maximum number of threads for the semaphore. In this example, both parameters are 1, so only one thread can use the resource at the same time.
A thread requests a resource by calling the Wait or WaitAsync method of the semaphore:
await semaphore.WaitAsync();
A thread releases a resource by calling the Release method of the semaphore:
semaphore.Release();
In the example below, we can see the usage of SemaphoreSlim:
SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
async Task AccessDatabase(string name, int seconds)
{
Console.WriteLine($"{name} is requesting access to the database");
await semaphore.WaitAsync();
Console.WriteLine($"{name} has access to the database");
await Task.Delay(TimeSpan.FromSeconds(seconds));
Console.WriteLine($"{name} is done with the database");
semaphore.Release();
}
AccessDatabase("Task 1", 2);
AccessDatabase("Task 2", 2);
In this example, two operations named “Task 1” and “Task 2” are trying to access the database. However, the semaphore only allows one operation to access the database at the same time. Therefore, when one operation completes its database access, the other operation can access the database.
In this diagram, two operations named “Task 1” and “Task 2” are requesting access to a SemaphoreSlim. The SemaphoreSlim only allows one operation to access at the same time. Therefore, when one operation completes its access, the other operation can access.
Consider a car park. A certain number of cars can park in the car park at the same time. We can use this situation as an analogy to understand how SemaphoreSlim works.
- Car park = resource (database, file, etc.)
- Cars = threads or tasks
- Capacity of the car park = first parameter of
SemaphoreSlim - The number of cars waiting to park = second parameter of
SemaphoreSlim - Waiting to park or after parking =
WaitAsyncorReleasemethods
This analogy can help to understand how SemaphoreSlim manages resources in a multi-threaded application.
The use of SemaphoreSlim with Task.WhenAll and Task.WaitAll is quite common to synchronize multiple tasks. These methods wait for multiple Tasks to complete.
Using with Task.WhenAll
The Task.WhenAll method waits for multiple Tasks to complete and returns a Task. The completion of this Task indicates that all Tasks have completed. Here is an example:
SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
async Task AccessDatabase(string name, int seconds)
{
Console.WriteLine($"{name} is requesting access to the database");
await semaphore.WaitAsync();
Console.WriteLine($"{name} has access to the database");
await Task.Delay(TimeSpan.FromSeconds(seconds));
Console.WriteLine($"{name
} is done with the database");
semaphore.Release();
}
Task task1 = AccessDatabase("Task 1", 2);
Task task2 = AccessDatabase("Task 2", 2);
await Task.WhenAll(task1, task2);
In this example, two tasks named “Task 1” and “Task 2” are trying to access the database. The Task.WhenAll method waits for both tasks to complete.
Using with Task.WaitAll
The Task.WaitAll method works like the Task.WhenAll method, but Task.WaitAll waits by blocking until all Tasks are complete. Here is an example:
SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
void AccessDatabase(string name, int seconds)
{
Console.WriteLine($"{name} is requesting access to the database");
semaphore.Wait();
Console.WriteLine($"{name} has access to the database");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
Console.WriteLine($"{name} is done with the database");
semaphore.Release();
}
Task task1 = Task.Run(() => AccessDatabase("Task 1", 2));
Task task2 = Task.Run(() => AccessDatabase("Task 2", 2));
Task.WaitAll(task1, task2);
In this example, two tasks named “Task 1” and “Task 2” are trying to access the database. The Task.WaitAll method waits for both tasks to complete.
By using these two methods, you can synchronize multiple tasks and control resource access with a SemaphoreSlim.
In conclusion, SemaphoreSlim allows only a specific number of threads to use a resource at the same time in a multi-threaded application. This is important especially in cases where more than one thread cannot use the same resource at the same time, otherwise there can be conflicts. We can use SemaphoreSlim to prevent deadlocks and manage resource access.
