Mutex is an OS-backed synchronization primitive that enforces single-owner access to a critical section. In .NET it is most useful for cross-process coordination via named mutexes — for example, ensuring only one instance of a Windows service writes to a shared log file, or preventing concurrent database migrations from two deployment slots. For purely in-process code, lock or SemaphoreSlim is a better default because they avoid the kernel transition overhead that makes Mutex one to two orders of magnitude slower than Monitor.Enter for uncontended acquisitions.
How It Works
Mutex derives from WaitHandle (it wraps an OS handle exposed through SafeWaitHandle), and has ownership semantics:
- A thread acquires ownership with
WaitOne. - Other waiters block until the owner releases it.
- The owning thread must call
ReleaseMutex. - Named mutexes can coordinate multiple processes on the same machine;
Global\/Local\prefixes are Windows Terminal Services scope controls, not general cross-platform naming defaults.
Mutex is reentrant (recursive). The owning thread can call WaitOne multiple times without blocking itself — but ownership is reference-counted, so it must call ReleaseMutex an equal number of times before another thread can acquire it. This differs from SemaphoreSlim, which has no thread affinity and is not reentrant (a recursive acquire on a 1-permit semaphore self-deadlocks).
using var m = new Mutex();
m.WaitOne(); // count = 1
m.WaitOne(); // count = 2 — same thread, does not block
// ...
m.ReleaseMutex(); // count = 1 — still owned
m.ReleaseMutex(); // count = 0 — now released for other threadsAcquiring multiple handles
Because Mutex is a WaitHandle, you can acquire several at once with WaitHandle.WaitAll (atomic — avoids the lock-ordering deadlocks of nested WaitOne calls) or race them with WaitAny:
var handles = new WaitHandle[] { mutexA, mutexB };
WaitHandle.WaitAll(handles); // acquire both atomically, no ordering deadlock
// ...
mutexB.ReleaseMutex();
mutexA.ReleaseMutex();WARNING
WaitAllis not supported on an STA thread (e.g. a WinForms/WPF UI thread) and throwsNotSupportedExceptionthere.
Example
using var mutex = new Mutex(initiallyOwned: false, name: "MyApp.SingleWriter");
if (!mutex.WaitOne(TimeSpan.FromSeconds(1)))
{
return;
}
try
{
WriteSharedFile();
}
finally
{
mutex.ReleaseMutex();
}Single-instance application guard using a global mutex:
// Global\ prefix makes the mutex visible across all Terminal Services sessions on Windows
const string MutexName = @"Global\MyApp.SingleInstance";
using var mutex = new Mutex(initiallyOwned: false, name: MutexName, createdNew: out bool created);
if (!mutex.WaitOne(0)) // non-blocking check
{
Console.Error.WriteLine("Another instance is already running.");
return;
}
try
{
RunApplication();
}
finally
{
mutex.ReleaseMutex();
}Pitfalls
- Kernel transition overhead on hot paths —
Mutex.WaitOneis a kernel call that costs 1-5 µs per uncontended acquisition, versus 20-50 ns forlock(which usesMonitor.Enterwith a user-mode spin before escalating). An API endpoint usingMutexfor in-process synchronization at 10K req/s adds 10-50 ms of cumulative wait time per second. Uselockfor in-process,Mutexonly when cross-process is required. - Release from wrong thread throws — calling
ReleaseMutexfrom a thread that does not own it throwsApplicationException. In async code where continuations can run on different threads, this is a landmine. KeepWaitOne/ReleaseMutexin the same synchronous method scope; for async patterns, useSemaphoreSlim.WaitAsyncinstead. - Abandoned mutex corruption risk — if the owner thread exits without releasing (crash, process kill, or an unhandled exception on the owning thread), the next waiter gets
AbandonedMutexException. This means the protected resource may be in an inconsistent state. Always validate shared state after acquiring an abandoned mutex. - Security on shared machines — named mutexes are not automatically restricted to your process or user context. On Windows, another process can open your named mutex and interfere with coordination. Use
MutexAccessRule/MutexSecurityto restrict access. On Linux, named mutexes use shared memory files with no ACL support — avoid named mutexes on multi-tenant machines. - Named-mutex lifetime is not the same across platforms — on Windows a named mutex is a kernel object that survives as long as a handle is open. On Linux/macOS, .NET backs named mutexes with shared-memory files under
/tmp(orTMPDIR) that are tied to the process/boot lifetime, not kernel-persistent; theGlobal\/Local\session prefixes are also Windows-only concepts. Don’t assume identical cross-machine semantics — for distributed single-instance guards use a real distributed lock (database row lock, RedisSETNX, lease), not a namedMutex.
Tradeoffs
Mutexvslock: mutex supports cross-process coordination, whilelockis faster and simpler for in-process synchronization.MutexvsSemaphore: mutex serializes to one owner; semaphore allows bounded parallel entrants.MutexvsSemaphoreSlim:SemaphoreSlimis better for in-process async throttling, but it cannot be named for cross-process locking.
Use lock in-process, Mutex only across processes on one machine. The kernel transition is the whole cost of a Mutex, so paying it for in-process work is waste when a lock would do. And if the coordination has to cross machines, neither works: reach for a distributed lock (a database row lock, a Redis lease), as the named-mutex pitfall above spells out.
Questions
When is a named
Mutexthe right tool in .NET?When you need to coordinate access across multiple processes on the same machine (for example single-writer protection for shared file/database artifacts).
Why is
Mutexoften a poor default for web request hot paths?It is OS-backed and blocking, so heavy contention can increase latency. In-process patterns (
lock,SemaphoreSlim, or aChannel<T>) are usually more efficient.
What does
AbandonedMutexExceptionsignal?A previous owner exited without releasing the mutex, which means exclusive ownership was recovered but shared state may be inconsistent and must be validated.
References
- Mutex class (Microsoft Learn) — ownership semantics,
ReleaseMutex,AbandonedMutexException, and the named-mutex constructor. - Overview of synchronization primitives (Microsoft Learn) — where
Mutexsits relative tolock/MonitorandSemaphoreSlim, and when the kernel object is worth its cost. - Managed threading best practices (Microsoft Learn) — guidance on wrong-thread release, abandoned mutexes, and cross-process coordination.
- Threading in C#: Event wait handles, mutexes, and semaphores (Joe Albahari) — mechanism-level treatment of
WaitHandle,WaitAll/WaitAny, and mutex reentrancy.