Thread Safety in .NET: lock, Semaphore and Mutex

Thread Safety in .NET: lock, Semaphore and Mutex

Thread Safety matters: in multi-threaded .NET applications, multiple threads often access shared data concurrently. Without proper synchronization, this can lead to race conditions or corrupted state. We need thread-safety mechanisms to ensure that only one thread manipulates a shared resource at a time. .NET provides several synchronization primitives; here we'll focus on three key ones: lock, Semaphore, and Mutex.


What are them?

Lock

In C#, lock is a keyword that marks a critical section. Only one thread can execute a lock block at a time, blocking others until the lock is released. It's the simplest way to enforce mutual exclusion within a single process. Think of it as a guarded section of code: if one thread is inside, others must wait.

Under the hood, lock uses an exclusive monitor - the thread that enters the block holds a lock on a given object, and no other thread can enter any lock on that same object until it's released. This ensures serialized access to the protected resource. The lock statement is fast and convenient for intra-process synchronization.

// Example 1: Using lock to protect a critical section
private static object _lockObj = new object();
private static int _sharedCounter = 0;

void IncrementSafely() {
    lock(_lockObj) {
        // Only one thread can execute this at a time
        _sharedCounter++;
        Console.WriteLine($"Counter incremented to {_sharedCounter}");
    }
}        

?? Starting from .NET 9, there is a Lock Class.

? Pros

  • Simple syntax with the lock keyword.
  • Fast and efficient for in-process synchronization.
  • Automatically released via compiler-managed try / finally blocks.
  • Supports reentrancy.

?? Cons

  • Limited to single process.
  • No built-in support for complex wait operations (e.g., WaitAny / WaitAll).

Semaphore

A semaphore is a counting synchronization mechanism that limits the number of threads that can access a resource simultaneously. It's like a nightclub with a fixed capacity enforced by a bouncer - once it's full, new entrants wait in line until someone leaves. A Semaphore (or SemaphoreSlim) starts with a specified count of available "slots". Each thread that enters (calls Wait) decrements the count, and when a thread exits (calls Release), the count increments, potentially allowing another waiting thread in.

For example, a semaphore initialized to 3 will let up to 3 threads run a certain code section in parallel; a 4th thread will wait until one of the first three exits. Importantly, a semaphore has no concept of ownership - any thread can release a slot back to the semaphore, not just the one that acquired it. This makes it a flexible throttling mechanism for protecting pools of resources (like limiting database connections or controlling concurrent I/O).

// Example 2: Using SemaphoreSlim to allow up to 3 threads concurrently
private static SemaphoreSlim _sem = new SemaphoreSlim(3); // 3 slots

void AccessResource() {
    _sem.Wait();                   // Wait (decrement) for an available slot
    try {
        Console.WriteLine($"{Thread.CurrentThread.Name} entered");
        // Simulate work
        Thread.Sleep(1000);
        Console.WriteLine($"{Thread.CurrentThread.Name} leaving");
    }
    finally {
        _sem.Release();            // Release (increment) the slot for others
    }
}        

There are two types of Semaphore:

  1. SemaphoreSlim: a lightweight, in-process semaphore that offers asynchronous support and minimizes kernel transitions.
  2. Semaphore: a full-featured, kernel-based semaphore that can be used for both in-process and cross-process synchronization (via naming).

? Pros

  • SemaphoreSlim offers async support and is lightweight for in-process scenarios.
  • Semaphore allows cross-process synchronization with naming.

  • Allows control over the number of concurrent threads.
  • Ideal for resource pooling or throttling.

?? Cons

  • Requires careful pairing of Wait and Release.
  • Incorrect use may lead to permit leaks or over-releases.
  • Kernel-based Semaphore (if used cross-process) incurs higher overhead.

Mutex

Mutex stands for Mutual Exclusion. It is similar to a lock in that it allows only one thread to access a resource at a time, but a key difference is scope: a Mutex can work across multiple processes. When one thread (in any process) owns a Mutex, no other thread (even in other applications on the system) can obtain it until it's released. In .NET, a Mutex is a system-wide named object (if you give it a name) or a local object (if unnamed).

Because it uses operating system handles, acquiring an uncontended Mutex is significantly slower (on the order of microseconds, roughly 50x slower than a lock in .NET). Like a lock, a Mutex is exclusive and also enforces thread ownership - the same thread that acquired it must release it (attempting to release from a different thread throws an exception). A common use-case for a named Mutex is to ensure only one instance of an application is running on a machine.

// Example 3: Using a Mutex for cross-process exclusivity (single instance guard)
using var mutex = new Mutex(false, "Global\\MY_APP_MUTEX");
if (!mutex.WaitOne(TimeSpan.Zero, false)) {
    Console.WriteLine("Another instance is already running. Exiting...");
    return;
}
// If WaitOne succeeds, we have the mutex.
// ... proceed with exclusive access (only one process can be here) ...

// (When done)
mutex.ReleaseMutex();        

Types of Mutexes:

  1. Unnamed Mutex: local to the current process; used for intra-process synchronization.
  2. Named Mutex: can be shared across processes, making it useful for cross-process resource protection.

? Pros

  • Unnamed Mutex provides simple mutual exclusion within one process.
  • Named Mutex enables cross-process synchronization with named instances.

  • Enforces thread ownership, making misuse easier to detect (e.g., abandoned mutex).
  • Compatible with WaitHandle APIs for complex scenarios.

?? Cons

  • Slower due to kernel-level context switches.
  • Must be released by the thread that acquired it.
  • Typically overkill for in-process locking.

Differences

Lock vs. Semaphore vs. Mutex (comparison table)

*Note: Semaphores don't enforce thread ownership; any thread can release a permit.

Why do we need them?

In concurrent applications, unsynchronized access to shared variables can cause unpredictable bugs. For example, two threads incrementing the same counter simultaneously might interfere with each other, leading to an incorrect result. Thread safety primitives like lock, semaphore, and mutex prevent such race conditions by coordinating threads. They make sure critical code executes atomically (one-at-a-time) and that resources (memory, files, etc.) are not accessed by multiple threads in conflicting ways. In short, these mechanisms preserve data consistency and program correctness under concurrency.

Lock

Use it to protect critical sections within a single process. It's lightweight and ideal for guarding in-memory data structures or any block of code that should not be executed by more than one thread at a time.

Semaphore

  • SemaphoreSlim

Use it when you need to limit concurrency within a single process in a resource-efficient way. It's great for asynchronous scenarios where you want to allow up to N threads to access a section of code concurrently while keeping overhead low.

  • Semaphore (Kernel-based)

Use it to limit concurrent access either in-process or across processes (with naming) when the scenario requires a counting mechanism provided by the OS. It's essential when you have a pool of identical resources or need to throttle calls (e.g., allowing up to N threads to access a service or database simultaneously).

Mutex

  • Named Mutex

Use it when you need a cross-process lock or when integrating with OS-level synchronization. If you have multiple processes or services that must not access the same resource concurrently (e.g., a file or a hardware device), a named Mutex can enforce that exclusivity system-wide.

  • Unnamed Mutex

Use it for simple mutual exclusion within a single process when you require explicit control over thread ownership, though in many cases a lock or SemaphoreSlim is preferred for performance.

Best Practices

  • Lock on private Objects

Always lock on a dedicated private object (avoid locking on this or string literals). If you have .NET 9+, use a Lock Class.

  • Match every Wait with a Release

Use try / finally blocks to ensure proper release and avoid deadlocks.

  • Keep critical sections short

Reduce the code inside locks to minimize blocking time.

  • Choose the right tool

- Use lock for simple in-process scenarios.

- Use SemaphoreSlim for efficient in-process throttling with async support.

- Use Semaphore for cross-process needs requiring a counting mechanism.

- Use Mutex (named or unnamed) when mutual exclusion is required, especially across processes.


Thread synchronization in .NET is about choosing the right tool for the job. Whether using a simple lock, a flexible Semaphore, or a cross-process Mutex, understanding these concepts is key to building robust and performant applications. Did you have a chance to work with all of them? Or you know other tools to handle the concurrency? Share your experience in the comments!


Babak Golkar

CTO at TRTech Enterprise System

15 小时前

GREAT share ??????

Victor Sankin

Owner | Angel Investor | Founder of @USE4COINS and @Abbigli | Blogger

1 周

Thread safety is critical in multi-threaded .NET applications! Choosing between lock, SemaphoreSlim, or Mutex depends on the use case—performance, scope, and resource sharing all matter. Great topic!

Elliot One

Entrepreneur | Founder @ XANT & Monoversity | Senior Software Enginer | Full Stack AI/ML Engineer | Engineering Intelligent SaaS & Scalable Software Solutions

1 周

Great topic! Choosing the right synchronization mechanism is key to balancing performance and safety in multi-threaded .NET apps.

要查看或添加评论,请登录

Ivan Vydrin的更多文章

  • Prototype - Pattern Clarity #10

    Prototype - Pattern Clarity #10

    The Prototype design pattern allows you to create new objects by cloning existing instances, avoiding the overhead of…

  • Types of Machine Learning

    Types of Machine Learning

    Machine Learning (ML) is a subset of Artificial Intelligence (AI) that enables computer systems to learn from data and…

    2 条评论
  • Chain of Responsibility - Pattern Clarity #9

    Chain of Responsibility - Pattern Clarity #9

    The Chain of Responsibility design pattern allows you to pass a request through a series of handlers (objects), where…

  • Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Vector Databases in AI/ML: the next-gen infrastructure for intelligent search

    Traditional databases struggle to handle AI-generated data like images, text, and audio embeddings. These…

    1 条评论
  • State - Pattern Clarity #8

    State - Pattern Clarity #8

    The State design pattern allows an object to alter its behavior when its internal state changes, making the object…

    4 条评论
  • Agentic AI: the rise of autonomous agents

    Agentic AI: the rise of autonomous agents

    Artificial Intelligence is evolving from simple tools into agentic systems that can act with autonomy and purpose…

    6 条评论
  • Singleton - Pattern Clarity #7

    Singleton - Pattern Clarity #7

    The Singleton design pattern ensures that a class has only one instance while providing a global point of access to it.…

    1 条评论
  • Exploring Types of Chatbots: an AI/ML perspective

    Exploring Types of Chatbots: an AI/ML perspective

    Chatbots have become an essential part of modern digital interactions, transforming customer service, sales, education,…

  • Observer - Pattern Clarity #6

    Observer - Pattern Clarity #6

    The Observer behavioral pattern establishes a one-to-many dependency between objects, so that when one object (the…

    4 条评论
  • Let's build a Free Web Chat Bot - Part 2/2

    Let's build a Free Web Chat Bot - Part 2/2

    In today's fast-moving digital world, smart and intuitive conversations give businesses a real edge. Azure AI Services…

社区洞察