1. The classic Coffman conditions
In 1971 computer scientist Edward Coffman (Edward G. Coffman Jr.) described four conditions without which deadlock is impossible. These rules have long become the "alphabet" for interviews and concurrency courses. It's useful not only to memorize them but to understand why they matter:
- Mutual Exclusion (Mutual Exclusion): at least one resource can be held by only one thread at a time.
- Hold and Wait (Hold and Wait): a thread holding one resource can wait for other resources.
- No Preemption (No Preemption): a resource cannot be forcibly taken from a thread — only the thread can release it.
- Circular Wait (Circular Wait): there is a chain of threads where each waits for a resource held by the next.
If all four conditions hold, deadlock is possible. If you break at least one — the chance of mutual blocking disappears.
Quick decision making
flowchart TD
A[Need to lock multiple resources?] -- No --> B[Standard lock]
A -- Yes --> C[Can you always take them in one order?]
C -- Yes --> D[Always take them in one order]
C -- No --> E[Avoid nested lock-s]
D --> F[Minimize time under lock]
E --> F
B --> F
F --> G{Still worried?}
G -- Yes --> H[Add timeouts and retries]
G -- No --> I[Sleep tight!]
2. Main strategies to prevent Deadlock
Since deadlock is an inherent part of the multithreaded world, our job is to learn to coexist with it. Different strategies help: from simple to sneaky.
Always lock resources in the same order
The idea is simple: if all threads always acquire resources in the same order, circular wait can't arise. This trick is often called ordered locking.
Example
Dangerous code:
// Thread 1
lock (resA)
{
lock (resB)
{
// ...
}
}
// Thread 2
lock (resB)
{
lock (resA)
{
// ...
}
}
Here deadlock is possible: the first thread grabbed resA and waits for resB, the second did the opposite. They both hang and never release.
Correct code:
// Always: first resA, then resB (never the other way)
lock (resA)
{
lock (resB)
{
// ...
}
}
Now the order is consistent and deadlock is impossible. It doesn't matter how many resources you have or what they're called — what matters is that all threads take locks in the same order.
Typical mistake: if there's even one place in code where the order is broken, the deadlock risk returns. So be careful!
Avoid locking multiple resources at once
This is the simplest: if you can avoid taking locks on two or more resources at once — don't take them! The more nested locks, the higher the risk of getting stuck. Instead you can, for example, copy needed data into temporary variables, release the lock and only then work with other resources.
Use timeouts when acquiring locks
If you really need to take multiple locks, timeouts help. For example, instead of the usual lock use methods that allow you to "try" to take the lock, and if it fails — unwind cleanly, for example Monitor.TryEnter.
object resourceA = new object();
object resourceB = new object();
bool successA = false, successB = false;
try
{
// Try to take both locks within 2 seconds
successA = Monitor.TryEnter(resourceA, TimeSpan.FromSeconds(2));
if (!successA) return; // didn't work, exit
successB = Monitor.TryEnter(resourceB, TimeSpan.FromSeconds(2));
if (!successB) return;
// Critical section
}
finally
{
if (successB) Monitor.Exit(resourceB);
if (successA) Monitor.Exit(resourceA);
}
If the second lock can't be taken within 2 seconds, we release the first and exit. Result: no deadlock, just some threads will back off and retry.
Minimize amount of code inside a lock
The smaller the locked section, the less time others wait. Don't do heavy computations, network calls or disk I/O inside a critical section. Grab the lock — quickly change shared state — release. Do everything else outside.
Partition state — avoid shared resources
Sometimes it's better not to fight locks but to avoid shared state:
- Use immutable objects (immutable).
- Work with copies or local variables instead of globals.
- Pass data by messages (Actor Model, message queues).
- Use thread-safe collections: ConcurrentDictionary, ConcurrentQueue and others from System.Collections.Concurrent.
3. Resolving Deadlock once it happened
Manually kill and restart (not the best option)
The simplest way is to terminate all blocked processes. But this risks data loss. It's a last resort you want to avoid.
Detecting Deadlock at runtime
Some systems can detect deadlock automatically. If a thread can't acquire a lock for too long, it can log the event, notify monitoring and start recovery.
if (!Monitor.TryEnter(resource, TimeSpan.FromSeconds(10)))
{
Console.WriteLine("Looks like we've hit a deadlock or someone's holding the lock too long!");
// You can initiate recovery, dump state or notify the user
}
In distributed systems, dedicated detectors analyze the wait-for graph and forcibly unlock stuck transactions.
Automatic rollback and retry
A handy pattern is to "give up" and retry: if you couldn't get all locks in reasonable time, roll back, wait a bit (often random to spread retries) and try again.
for (int attempt = 0; attempt < 3; attempt++)
{
bool gotRes1 = Monitor.TryEnter(res1, TimeSpan.FromSeconds(2));
bool gotRes2 = false;
try
{
if (gotRes1)
{
gotRes2 = Monitor.TryEnter(res2, TimeSpan.FromSeconds(2));
if (gotRes2)
{
// Critical section
break;
}
}
}
finally
{
if (gotRes2) Monitor.Exit(res2);
if (gotRes1) Monitor.Exit(res1);
}
// Didn't work — wait and try again
Thread.Sleep(500 + new Random().Next(500));
}
4. Best practices and recommendations
- Analyze lock acquisition order. Gather entry points and verify ordering.
- Prefer collections from System.Collections.Concurrent when possible.
- Use static analysis tools. IDEs like Rider or Visual Studio help find nested locks.
- Log long lock attempts.
- Stress-test multithreaded components.
- Document the lock ordering conventions. Comments save you from order violations.
5. Practical examples
Example: Database
In DBMSs deadlock is a frequent guest: two transactions update the same tables but in different order (A→B and B→A). Most DBMSs can detect such situations and "kill" one of the transactions.
Example: Asynchronous methods
In .NET, mixing async and locks can produce deadlock: await can block a thread holding a lock, while another thread waits for the same resource. Plan critical section boundaries and avoid awaits inside them.
6. Typical mistakes when working with locks
Mistake #1: locking strings.
lock (myString) — a bad idea. In .NET strings are interned, so you effectively lock the global string table.
Mistake #2: different lock acquisition order.
If different parts of the program acquire locks in different orders, the likelihood of deadlock rises sharply.
Mistake #3: overly long locks.
Holding a lock longer than necessary or calling external methods inside a critical section is a sure way to get hangs and mutual blocking.
GO TO FULL VERSION