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.

Post a Comment

Please do not post any spam link in the comment box😊

Previous Post Next Post

Blog ads

CodeGuru