DEV Community

Cover image for Multithreading and Synchronization in C# ๐Ÿš€
Mo
Mo

Posted on

Multithreading and Synchronization in C# ๐Ÿš€

Hello everyone, Today, we're diving into some essential concepts in multithreading and synchronization in C#. We'll be discussing lock, Mutex, Semaphore, and SemaphoreSlim. These tools are crucial for managing concurrent access to resources in your applications. Without further ado, letโ€™s get started! ๐ŸŒŸ

The lock Statement ๐Ÿ”’

The lock keyword in C# ensures that a block of code runs by only one thread at a time. It's a simple way to prevent race conditions.

How lock Works

At compile time, lock is converted to Monitor and is surrounded by a try-finally block. In the finally block, the thread is released even if there's an exception. So, lock is essentially a simpler way of using Monitor without writing try-finally statements each time!

Hereโ€™s an example to illustrate this:

object lockObject = new object();

int counter = 0;

int safeCounter = 0;

Parallel.For(0, 10000, i =>
{
    counter++;

    lock (lockObject)
    {
        safeCounter++;
    }
});


Console.WriteLine($"Counter: {counter}");
Console.WriteLine($"Counter with lock: {safeCounter}");
Enter fullscreen mode Exit fullscreen mode

In this example, we have a parallel loop iterating from 0 to 10000. It increases the values of counter and safeCounter, but safeCounter is surrounded by the lock statement.

In the first iteration, if three threads are running simultaneously and each reads the value of counter (which is one), they will all increase the value to two. This means that the counter will be two for all three threads, resulting in a race condition.

The lock syntax prevents multiple threads from accessing safeCounter simultaneously, ensuring that only one thread can access it at a time. This results in a sequential increase in value (e.g., one, two, three, four). As a result, we get the expected incremental value.

When we run the project, the value for the counter is unexpectedly different due to race conditions, so it may vary on your computer. However, the value for the safeCounter with the lock is 10000.

Synchronizing External Processes ๐ŸŒ

Now, we might wonder what would happen if multiple external processes need to access a shared resource, considering we have internal threads in one process in this example.

Imagine having a shared resource like a file and multiple processes (e.g., Process One and Process Two). In this scenario, using lock won't help because it's designed for internal threads within a process, not for synchronizing external processes.

Synchronization Solutions ๐Ÿ› ๏ธ

For better clarification, let's consider finding a synchronization solution in a multithreaded application with two categories:

Internal Process Scenarios

Single Thread:

  • Use the lock statement.

Multiple Threads:

  • Use SemaphoreSlim.

External Process Scenarios

Single Thread:

  • Use a Mutex.

Multiple Threads:

  • Use a Semaphore.

Examples for Mutex and Semaphore

Mutex Example

A Mutex can be used to synchronize threads across different processes.

using Mutex mutex = new Mutex(false, "GlobalMutex");

if (!mutex.WaitOne(TimeSpan.FromSeconds(5), false))
{
    Console.WriteLine("Another instance is running, exiting...");
    return;
}

Console.WriteLine("No other instance is running, proceeding...");
Console.ReadLine();
Enter fullscreen mode Exit fullscreen mode

Semaphore Example

A Semaphore can be used to limit the number of threads that can access a resource simultaneously.

class Program
{
    static Semaphore _semaphore = new Semaphore(2, 2);

    static void Main()
    {
        for (int i = 0; i < 5; i++)
        {
            Thread t = new Thread(Worker);
            t.Start();
        }
    }

    static void Worker()
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is waiting...");
        _semaphore.WaitOne();

        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is working...");
        Thread.Sleep(2000);

        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is releasing...");
        _semaphore.Release();
    }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up ๐ŸŽฌ

I hope you found this explanation of lock, Mutex, Semaphore, and SemaphoreSlim helpful. If you did, make sure to give this article a thumbs up ๐Ÿ‘ and subscribe to my YouTube channel for more programming tutorials. Thanks for reading, and Iโ€™ll see you in the next one! ๐Ÿ‘‹

Top comments (0)