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.

SemaphoreSlim Diagram

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 or Release 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 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.



More posts like this

LINQ and Optimistic Concurrency: Methods for Ensuring Data Integrity

2023-06-11 | #linq #net #optimistic-concurrency

When working with databases, it is of great importance to properly manage concurrent operations and ensure data integrity. Concurrency control is used to manage multiple users or processes accessing the same data concurrently. In this article, we will explore how to ensure data integrity using LINQ (Language Integrated Query) with optimistic concurrency. What is Optimistic Concurrency? Optimistic concurrency is a method where resources are not locked when a transaction begins, but changes are checked before the transaction is completed.

Continue reading 


LINQ and Pessimistic Concurrency: Methods for Ensuring Data Integrity

2023-06-11 | #linq #net #pessimistic-concurrency

When working with databases, managing concurrent operations and ensuring data integrity are of great importance. Concurrency control is used to manage multiple users or processes accessing the same data concurrently. In this article, we will explore how to use LINQ (Language Integrated Query) with pessimistic concurrency to maintain data integrity. What is Pessimistic Concurrency? Pessimistic concurrency is based on the principle of locking the relevant data when a transaction begins and maintaining the lock until the transaction is completed.

Continue reading 