DEV Community

Ahmed Omeiza
Ahmed Omeiza

Posted on

Deadlocks: When Your Threads Are Waiting Forever

Your application is running. No exception. No crash.

But two requests are stuck indefinitely.

Welcome to a deadlock.

A deadlock happens when two or more threads are waiting for resources held by each other, so none of them can continue.


What Is a Deadlock?

Imagine two developers:

  • Developer A has Resource 1 and needs Resource 2.
  • Developer B has Resource 2 and needs Resource 1.

Both are waiting.

Neither can move forward.

That's essentially what happens with threads and locks.

Thread A → holds Lock 1 → waiting for Lock 2
Thread B → holds Lock 2 → waiting for Lock 1

Result: DEADLOCK
Enter fullscreen mode Exit fullscreen mode

The important part is that both threads are waiting for each other.


A Simple C# Example

Consider this:

object lockA = new();
object lockB = new();

void MethodA()
{
    lock (lockA)
    {
        Thread.Sleep(100);

        lock (lockB)
        {
            Console.WriteLine("Method A");
        }
    }
}

void MethodB()
{
    lock (lockB)
    {
        Thread.Sleep(100);

        lock (lockA)
        {
            Console.WriteLine("Method B");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If two threads execute these methods at the same time:

Thread 1:
Locks A
↓
Waits for B

Thread 2:
Locks B
↓
Waits for A
Enter fullscreen mode Exit fullscreen mode

Neither thread can acquire the lock it needs.

The application can remain stuck indefinitely.


Why Do Deadlocks Happen?

Deadlocks usually involve a combination of these conditions:

1. Mutual Exclusion

A resource can only be used by one thread at a time.

Thread A → owns Resource X
Thread B → must wait
Enter fullscreen mode Exit fullscreen mode

2. Hold and Wait

A thread holds one resource while waiting for another.

Thread A:
Holding X
Waiting for Y
Enter fullscreen mode Exit fullscreen mode

3. No Preemption

A resource cannot simply be taken away from the thread holding it.

The thread must release it.

4. Circular Wait

Thread A waits for Thread B, while Thread B waits for Thread A.

A → waiting for B
B → waiting for A
Enter fullscreen mode Exit fullscreen mode

This circular dependency is what ultimately creates the deadlock.


How Do You Prevent Deadlocks?

One of the simplest strategies is to always acquire locks in the same order.

Instead of:

// Method A
lock (lockA)
{
    lock (lockB)
}
Enter fullscreen mode Exit fullscreen mode

and:

// Method B
lock (lockB)
{
    lock (lockA)
}
Enter fullscreen mode Exit fullscreen mode

Make both follow the same order:

lock (lockA)
{
    lock (lockB)
    {
        // Work
    }
}
Enter fullscreen mode Exit fullscreen mode

Now both threads agree:

Lock A → Lock B
Enter fullscreen mode Exit fullscreen mode

There is no circular waiting.


Other Ways to Reduce the Risk

Keep Lock Scope Small

Don't hold a lock longer than necessary.

lock (resource)
{
    // Only critical work
}
Enter fullscreen mode Exit fullscreen mode

Avoid performing slow operations such as network calls or database requests while holding a lock.

Avoid Unnecessary Multiple Locks

The more locks you need to coordinate, the more opportunities you create for circular dependencies.

Use Async Carefully

Async code can introduce its own synchronization problems.

Avoid blocking asynchronous operations with:

.Result
Enter fullscreen mode Exit fullscreen mode

or:

.Wait()
Enter fullscreen mode Exit fullscreen mode

Prefer:

await SomeOperationAsync();
Enter fullscreen mode Exit fullscreen mode

This doesn't automatically eliminate every deadlock, but it avoids an important class of blocking problems.


Deadlock vs Race Condition

These problems are often confused.

A race condition happens when multiple threads access shared data and the result depends on the timing of their execution.

A deadlock happens when threads are stuck waiting for each other.

Think of it this way:

Race condition:
"Who gets there first?"

Deadlock:
"Nobody can move."


Key Takeaway

A deadlock isn't necessarily caused by a bug that crashes your application.

Sometimes the more dangerous bug is the one that makes your application wait forever.

When working with multiple locks, remember:

Acquire locks consistently, keep lock scope small, and avoid unnecessary blocking.

Good concurrency isn't just about making multiple things run at once. It's also about making sure they can actually finish.

Top comments (0)