📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a Mutex in .NET, how is it different from lock, and when would you use a named Mutex?

Kernel mutex, cross-process named mutexes, Global and Local prefixes, AbandonedMutexException, thread affinity, cost and single-instance apps.

Mid-Level Theory Intermediate cross-process kernel-objects mutex synchronization

🎯 Key points

  • A Mutex is a kernel object for mutual exclusion
  • Named mutexes work across processes on one machine
  • Global\ and Local\ prefixes control session scope on Windows
  • Mutex is thread-affine and re-entrant
  • AbandonedMutexException means the previous owner died; validate state
  • Much slower than lock; not usable across awaits
  • Use distributed locks for multiple machines

⏱ 30-second answer

A Mutex is an operating-system mutual-exclusion object. Unlike lock, it can be named and shared between processes on the same machine, which is its main use: single-instance applications or protecting a shared file. It is much slower than lock because every acquire involves the kernel, it is thread-affine (the owning thread must release it) and re-entrant. If the owner exits without releasing, the next waiter gets AbandonedMutexException, which tells you the protected state might be inconsistent. It is not suitable for async code.

🗣 Full answer (senior)

How I would answer this in an interview

"A Mutex is an operating-system mutual-exclusion object. Inside one process it does the same job as lock, but much more expensively, since every acquire is a kernel call. So I don't use unnamed mutexes. Its real value is that it can be named and shared by processes on the same machine: single-instance applications, or making sure two tools never write the same local file at the same time. Like lock it's thread-affine (the owning thread must release it) and re-entrant (the owner can acquire again but must release once per acquire). It has one feature lock lacks: if the owner dies without releasing, the next waiter gets AbandonedMutexException. It still owns the mutex, but that's a warning that the protected state may be half-written. It's not usable across await, and it's machine-local, so for multiple servers I use a lease-based distributed lock instead."


1. lock vs Mutex

Aspect lock / Monitor Mutex
Scope One process Process, or machine-wide when named
Implementation User-mode fast path (object header / Lock) Kernel object (Windows) / runtime-emulated on Unix
Cost (uncontended) Tens of nanoseconds Microseconds (system call)
Thread-affine Yes Yes
Re-entrant Yes Yes (release once per acquire)
Abandonment detection No AbandonedMutexException
Timeouts Monitor.TryEnter WaitOne(timeout)
Async-friendly No No
Wait on several handles No WaitHandle.WaitAny/WaitAll

2. Named mutex usage

using var mutex = new Mutex(initiallyOwned: false, @"Global\Contoso.InvoiceExporter");
if (!mutex.WaitOne(TimeSpan.Zero))
{
    Console.WriteLine("Another instance is running");
    return;
}
try { Run(); }
finally { mutex.ReleaseMutex(); }
Name prefix (Windows) Visibility
Global\name All sessions on the machine (e.g. services + interactive users)
Local\name or no prefix Current terminal-server session only

On Linux and macOS, .NET emulates named mutexes with shared files in a temp location: they work across processes on the same machine that share that location. Separate containers don't share named mutexes unless they share the underlying filesystem/namespace.

3. Abandonment

Situation What happens
Owner thread exits without ReleaseMutex OS releases it; next WaitOne throws AbandonedMutexException but the caller now owns the mutex
Owner process is killed Same
Correct handling Catch it, treat the mutex as owned, validate or repair the shared resource, then continue

4. When (not) to use it

Need Tool
Threads in one process lock / System.Threading.Lock
Async critical section SemaphoreSlim(1, 1)
Single-instance desktop/CLI app Named Mutex
Two processes on one machine touching one file Named Mutex, or exclusive file open (FileShare.None)
Async across processes File lock, database application lock
Multiple machines / pods DB application lock (sp_getapplock), Redis lock, blob lease, coordination service, with expiry

Production example

Our [scheduled invoice exporter] could be started both by Windows Task Scheduler and manually by support staff. Roughly [once a month] partners received garbled CSV files. Process start logs showed two exporter instances running at the same time, both writing the same output path. I added a Global\ named mutex acquired with WaitOne(TimeSpan.Zero), so a second instance logs "already running" and exits cleanly. I also handled AbandonedMutexException (if a previous run crashed mid-export, the new run validates and regenerates the partial file before continuing), and kept all mutex-owning work synchronous. The corruption stopped; we also alerted when a second instance attempted to start. When the exporter later moved to two Kubernetes pods, we replaced the mutex with a database application lock, because a mutex can't coordinate across machines.

🔍 What the interviewer is checking

Checks understanding of OS-level primitives and their scope.

Green flags

  • Explains named cross-process use and Global\/Local\.
  • Knows thread affinity and abandonment.
  • States cost compared with lock.
  • Proposes distributed locks for multi-server.

Red flags

  • Uses Mutex as a general in-process lock.
  • Holds a mutex across awaits.

📚 Learn in depth

1. What "kernel object" means

On Windows, new Mutex(...) calls CreateMutexEx, producing a kernel object with a handle in the process's handle table. WaitOne calls a wait function (WaitForSingleObjectEx); ReleaseMutex calls ReleaseMutex. Every operation crosses into kernel mode, which is why it costs microseconds, even uncontended. lock stays in user mode unless it must block.

2. Thread affinity in practice

mutex.WaitOne();
await Task.Delay(10);       // may resume on another thread
mutex.ReleaseMutex();       // ApplicationException: "Object synchronization method was called from an unsynchronized block of code"

The original thread is still recorded as the owner. If it exits, the mutex becomes abandoned. Async code must never hold a mutex across await.

3. Holding a mutex for an app's lifetime

For single-instance apps, keep the mutex referenced (a static field) until exit. If it's only a local and the JIT/GC decides it's dead, it can be finalised and released early, allowing a second instance. GC.KeepAlive(mutex) at the end of Main or a static field avoids this.

4. Waiting on several handles

WaitHandle[] handles = [mutex, shutdownEvent];
int index = WaitHandle.WaitAny(handles, TimeSpan.FromSeconds(30));

This is one capability lock doesn't offer: wait for "lock acquired OR shutdown requested". WaitAll with a mutex in an STA thread isn't supported.

5. Security and naming

  • Named objects live in a shared namespace. Use specific names (Company.Product.Purpose).
  • On Windows, a hostile process could pre-create the name; for sensitive cases set ACLs (MutexAcl.Create, Windows only).
  • Avoid user-controlled names.

6. Distributed alternatives

Option Notes
SQL Server sp_getapplock / PostgreSQL advisory locks Tied to a session/transaction; released if the connection dies
Redis lock (single-instance SET NX PX, or Redlock) Needs expiry; beware of clock and failover edge cases
Azure Blob lease Time-bounded lease, renewed while working
Kubernetes Lease / leader election For "one active instance" patterns

All require expiry + fencing: assume a lock holder can pause (GC, network) past its lease and design writes to be idempotent or guarded by a fencing token.

7. Version notes

Version Change
.NET Framework 1.0 Mutex
.NET Framework 2.0 AbandonedMutexException thrown by default
.NET Core 2.0+ Named mutexes on Linux/macOS (runtime emulation)
.NET 5+ MutexAcl (Windows) for ACLs
.NET 10 Same core semantics

8. Try it yourself

  1. Run the code example twice in parallel; the second exits.
  2. Start one instance, kill -9 / End Task it while a third instance blocks in WaitOne(); catch AbandonedMutexException in the third.
  3. Benchmark 1 million uncontended lock vs Mutex.WaitOne/ReleaseMutex pairs.
  4. Add await Task.Delay(1) between WaitOne and ReleaseMutex with ConfigureAwait(false) and observe the exception.
  5. Run two Docker containers with the same mutex name and see that they don't exclude each other.
  6. Use WaitHandle.WaitAny with a mutex and a ManualResetEvent for shutdown.

💻 Code

using System.Threading;

const string Name = @"Global\Contoso.InvoiceExporter";
using var mutex = new Mutex(initiallyOwned: false, Name);

bool owned;
try
{
    owned = mutex.WaitOne(TimeSpan.Zero); // do not wait: single instance
}
catch (AbandonedMutexException)
{
    owned = true; // we own it, but the previous owner died mid-run
    Console.WriteLine("Previous run crashed; validating output file first");
}

if (!owned)
{
    Console.WriteLine("Another exporter is running. Exiting.");
    return;
}

try
{
    Console.WriteLine("Exporting invoices...");
    Thread.Sleep(2000); // synchronous work only: the mutex is thread-affine
}
finally
{
    mutex.ReleaseMutex();
}

⚠ Common mistakes

1. Unnamed Mutex instead of lock

What happens: new Mutex() guards in-process state. Why it is wrong: kernel calls every time; no benefit over lock. Do instead: lock/Lock; use Mutex only for named, cross-process exclusion.

2. Holding a mutex across await

What happens: WaitOne(); await ...; ReleaseMutex();. Why it is wrong: the continuation may run on another thread → ApplicationException; the original thread still owns it. Do instead: keep owning code synchronous, or use a file/DB lock for async work.

3. Ignoring AbandonedMutexException

What happens: the exception is swallowed or crashes the app. Why it is wrong: you own the mutex, but the resource may be half-written. Do instead: catch it, treat as owned, validate/repair the resource.

4. Using a named mutex for multiple servers

What happens: "only one instance" logic is relied on in a scaled-out deployment. Why it is wrong: it's machine-local (and container-local). Do instead: distributed lock or lease with expiry and fencing.

5. Forgetting to release on every path

What happens: an exception skips ReleaseMutex. Why it is wrong: the mutex stays owned until the thread exits (then becomes abandoned). Do instead: try/finally.

6. Releasing a mutex you didn't acquire

What happens: ReleaseMutex() after WaitOne(0) returned false. Why it is wrong: throws ApplicationException. Do instead: track whether you own it and release only then.

7. Letting a single-instance mutex be collected

What happens: the mutex is a local in Main that the GC considers dead. Why it is wrong: it can be finalised and released, so a second instance starts. Do instead: static field or GC.KeepAlive.

8. Generic or user-supplied names

What happens: new Mutex(false, "Global\\App"). Why it is wrong: collisions with other software, or hostile pre-creation. Do instead: specific names; ACLs for sensitive cases.

9. Using initiallyOwned: true and ignoring createdNew

What happens: code assumes it owns the mutex after construction. Why it is wrong: if the mutex already existed, the caller doesn't own it. Do instead: use out bool createdNew or call WaitOne explicitly.

10. Expecting fairness

What happens: code assumes waiters acquire in arrival order. Why it is wrong: OS wait ordering isn't a guarantee you should depend on. Do instead: design without fairness assumptions; use a queue if order matters.

🏛 Senior insight: trade-offs

Mutex is a machine-level tool. I use it for single-instance tools and shared local files, and I think about abandonment explicitly. For anything distributed or async I pick a lease-based lock with expiry instead.

Scope of exclusion Tool
Threads in one process lock, SemaphoreSlim
Processes on one machine Named Mutex, file lock
Many machines Database app lock, Redis or blob lease
Async across processes File or database lock

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says lock is faster in-process Why: Checks lock fundamentals
    How to answer

    lock (x) { ... } gives mutual exclusion: only one thread at a time runs the block for the same lock object, and entering and leaving act as memory barriers. For a normal object the compiler emits Monitor.Enter(x, ref lockTaken) in a try/finally with Monitor.Exit. Since .NET 9 and C# 13, if x is a System.Threading.Lock, the compiler instead uses x.EnterScope() and disposes the scope, which is a dedicated, slightly faster lock type. Locks are re-entrant on the same thread, you cannot await inside one (CS1996), and you should lock on a private, readonly object and keep the block short.

  2. When: Candidate mentions counting or N holders Why: Checks semaphore semantics
    How to answer

    A semaphore holds a count of available slots: WaitOne takes a slot (blocking if the count is zero) and Release returns one, so up to N threads can be inside at once. System.Threading.Semaphore is the kernel-backed version; it can be named for cross-process use on Windows (named semaphores are not supported on Linux or macOS). Unlike a Mutex, it has no owner: any thread can release it, and it is not re-entrant. Releasing past the maximum throws SemaphoreFullException. For in-process and async throttling, use SemaphoreSlim instead.

🛠 Real-world problems: how you handle them

Two exporter instances corrupting the same file
Situation
A [scheduled invoice exporter] could be started by Task Scheduler and manually by support staff.
Signals
Partners received [garbled CSV files] roughly [once a month].
Investigation
Process start logs showed two instances running at the same time writing the same output path.
Resolution
Added a `Global\` named mutex with `WaitOne(0)` so a second instance logs and exits; handled `AbandonedMutexException` by validating the file.
Prevention
Documented the single-instance guarantee and added an alert when a second instance starts.

How to tell it (STAR)

We had [monthly corrupt exports]. Logs showed two exporter instances at once, one from the scheduler and one started manually. I added a named global mutex so the second copy exits, and handled the abandoned case by validating the file. The corruption stopped.

↩ Usually asked after

More in Synchronization

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me