The SemaphoreSlim Class: Multitask-Based Programming in C#
[tr] Türkçe Oku 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 =
WaitAsync
orRelease
methods
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 Task
s to complete.
Using with Task.WhenAll
The Task.WhenAll
method waits for multiple Task
s to complete and returns a Task
. The completion of this Task
indicates that all Task
s 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 Task
s 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.