1. Introduction
Recall the example from the previous lecture: two threads in our still-simple app increment a shared counter, but the final value doesn't always match what we expect. To protect that counter we already used the keyword lock (or, more precisely, Monitor), which is suitable for synchronization only inside one process. But what if your program isn't the only one that wants to use the resource? For example, you're writing some super-service that can be started in two instances and both want to write to the same file… or control hardware like a printer port? That's when the good old mutex (from mutual exclusion) comes in handy.
Concept
A mutex is a synchronization primitive that not only limits access to a resource between threads inside a single process, but also allows coordinating access between different processes on the same machine. Think of it as a big "Occupied" sign outside a meeting room that both staff and visitors can see.
In .NET this is provided by the class System.Threading.Mutex.
When Mutex is actually needed:
- When you synchronize access between threads in different processes (for example, two separate applications working on the same file).
- When the resource is so valuable and indivisible that even process context can't share access rights.
For synchronization only between threads of one process you usually use lock (Monitor). Mutex is preferable for interprocess synchronization because it's heavier and slower.
How Mutex works
flowchart TD
A(Process 1) --|Requests|--> M(Mutex)
B(Process 2) --|Requests|--> M
M --|Only one allowed|--> R(Shared resource)
A --|Releases|--> M
B --|After release|--> M
2. Basic syntax for working with Mutex
Creation
A Mutex is created just like most other synchronization classes:
using System.Threading;
Mutex mutex = new Mutex();
Main methods
- WaitOne() — attempt to acquire the mutex; if it fails — the thread blocks until someone else releases the mutex.
- ReleaseMutex() — releases the mutex, allowing other threads or processes to enter the critical section.
Simple example: synchronizing between threads inside one process
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // Enter critical section
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // Exit critical section
Thread.Sleep(100); // For clarity
}
}
}
In this example both threads take turns accessing the console to print.
3. Interprocess synchronization with a named mutex
For the heavy artillery — synchronization between processes — a named mutex is used. You give it a name, and all processes on the machine can refer to it.
Mutex mutex = new Mutex(false, "MyApp_Mutex");
Constructor parameters:
- The first parameter (bool initiallyOwned) — whether the thread wants to own the mutex immediately after creation. Usually — false.
- The second parameter — the mutex name. All processes that use a mutex with that name refer to the same object, for example "MyApp_Mutex".
Example: two applications with one Mutex
Run the same program in two different windows to see the effect.
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("Trying to enter critical section...");
mutex.WaitOne(); // Wait until the other process releases the mutex
try
{
Console.WriteLine("Critical section is owned by this process.");
Console.WriteLine("Press Enter to leave the critical section.");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("Critical section released.");
}
}
}
}
Try this:
- Open two windows with this application.
- Run both — the second will wait until you press Enter in the first.
4. Single-instance application
Mutex is often used to limit how many instances run at the same time. For example: "Hey user, next time don't open two copies of the calculator!"
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("Application is already running!");
return;
}
Console.WriteLine("Application started successfully. Press Enter to exit.");
Console.ReadLine();
}
}
}
This pattern is common in desktop apps: the first instance starts, the second just reports that and exits. The official Microsoft example is in the documentation.
5. Common mistakes when working with Mutex
Mistake #1: forgot to call ReleaseMutex().
If a thread successfully acquired the mutex (WaitOne()) but didn't call ReleaseMutex() (crashed with an exception or simply forgot), then no other thread or process can enter until the "forgetful" thread exits. This can cause a deadlock. Good style is to always use try-finally:
mutex.WaitOne();
try
{
// critical section
}
finally
{
mutex.ReleaseMutex();
}
Mistake #2: wrong number of ReleaseMutex() calls.
If you call ReleaseMutex() more times than WaitOne() was called, an ApplicationException will be thrown.
Mistake #3: trying to release a Mutex you don't own.
A mutex is "bound" to the thread that acquired it. Only that thread has the right to call ReleaseMutex(). If another thread tries to release it, .NET will throw an exception.
Mistake #4: using Mutex where lock is enough.
Mutex is slower than a normal lock because it can work between processes and requires system calls. Recommendation: if you don't need interprocess synchronization — use lock.
Mistake #5: unlucky mutex name.
If you pick a too-simple name (for example, "MyMutex"), you might accidentally "befriend" some other program that uses the same name. Better use unique names (for example, including your company or app name).
6. Useful nuances
WaitOne(timeout)
You can set a timeout for waiting for the mutex:
if (mutex.WaitOne(5000)) // wait up to 5 seconds
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("Failed to get access to the resource within 5 seconds!");
}
Mutex.TryOpenExisting
If you need to connect to an already existing mutex, use the static method:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// now existingMutex is a reference to the existing mutex
}
Separation between users
By default a named mutex is available to all users with rights to create system objects. If you need stricter control — use the constructor with MutexSecurity.
Comparison table: lock/Monitor, Mutex, Semaphore
| Primitive | Interprocess synchronization? | Speed | Where to use |
|---|---|---|---|
|
No | Very high | Between threads in one process |
|
Yes | Lower (more expensive) | Between threads of different processes |
|
Yes (for NamedSemaphore) | Comparable to Mutex | When you need to limit the number of threads |
More about semaphores — in the next lecture.
States of Mutex
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : thread "died" without calling ReleaseMutex()
Abandoned --> Unowned
- Unowned — nobody owns the mutex.
- Owned — the mutex is held by a thread.
- Abandoned — a thread terminated unexpectedly without calling ReleaseMutex(). The next one who gets the mutex will receive an AbandonedMutexException (this is a sign of a bug and potentially an inconsistent state of the resource).
GO TO FULL VERSION