Thread Synchronization and Semaphores in C#

Concurrent access to shared resources like databases is a common challenge in software development. To ensure data integrity and prevent conflicts, synchronization mechanisms like semaphores are crucial. In this blog post, we’ll explore how to use C#'s SemaphoreSlim to manage concurrent database access. We’ll simulate multiple threads reading and writing data to a database and use semaphores to maintain orderly access.

Table of Contents

  1. Understanding Semaphores
  2. Scenario: Simulating Concurrent Database Access
  3. Implementing Semaphore-Controlled Database Access
  4. Running the Simulation
  5. Conclusion

1. Understanding Semaphores

Before we proceed, let’s briefly recap semaphores and their role in managing concurrent access.

A semaphore is a synchronization primitive that restricts the number of threads concurrently accessing a shared resource. It uses a counter to manage access, with the Wait method decrementing the counter for access and the Release method incrementing it when done.

2. Scenario: Simulating Concurrent Database Access

Imagine you’re building an application with multiple threads that read and write data to a database. Without proper synchronization, simultaneous writes can lead to data inconsistencies or even data corruption.

3. Implementing Semaphore-Controlled Database Access

Let’s create a C# class called DatabaseManager that will handle database access using a SemaphoreSlim. By controlling the access with semaphores, we can ensure that only a limited number of threads can access the database simultaneously.

using System;
using System.Threading;
using System.Threading.Tasks;

class DatabaseManager
{
    private SemaphoreSlim _databaseSemaphore;
    private Random _random = new Random();

    public DatabaseManager(int maxConcurrentAccess)
    {
        _databaseSemaphore = new SemaphoreSlim(maxConcurrentAccess);
    }

    public async Task ReadDataAsync(int threadId)
    {
        await _databaseSemaphore.WaitAsync();
        try
        {
            Console.WriteLine($"Thread {threadId} is reading data from the database.");
            await Task.Delay(_random.Next(100, 500)); // Simulate reading
        }
        finally
        {
            _databaseSemaphore.Release();
        }
    }

    public async Task WriteDataAsync(int threadId)
    {
        await _databaseSemaphore.WaitAsync();
        try
        {
            Console.WriteLine($"Thread {threadId} is writing data to the database.");
            await Task.Delay(_random.Next(100, 500)); // Simulate writing
        }
        finally
        {
            _databaseSemaphore.Release();
        }
    }
}
(ads)

4. Running the Simulation

In the Main method of your program, you can create an instance of DatabaseManager and simulate multiple threads reading and writing to the database:

static async Task Main(string[] args)
{
    int maxConcurrentAccess = 2; // Maximum concurrent database access
    int totalThreads = 5; // Total threads to simulate

    var databaseManager = new DatabaseManager(maxConcurrentAccess);

    var tasks = new Task[totalThreads];
    for (int i = 0; i < totalThreads; i++)
    {
        int threadId = i + 1;
        tasks[i] = Task.Run(async () =>
        {
            await databaseManager.ReadDataAsync(threadId);
            await databaseManager.WriteDataAsync(threadId);
        });
    }

    await Task.WhenAll(tasks);

    Console.WriteLine("All threads completed.");
}

5. Conclusion

Using SemaphoreSlim for concurrent database access is a powerful technique to ensure data consistency and prevent conflicts in multithreaded applications. In this example, we demonstrated how to use semaphores to control access to a shared database resource. By employing semaphores, you can avoid race conditions and maintain data integrity, crucial for reliable and stable software systems.

Next Post Previous Post
No Comment
Add Comment
comment url