1. Introduction
Livelock is a situation in a multithreaded application where two or more threads try to avoid mutual blocking (Deadlock), but end up endlessly reacting to each other’s actions without making progress. Unlike Deadlock, where threads are frozen and can't do anything, in Livelock the program looks alive: threads are running, but none of them does useful work.
It's like you and a colleague in a narrow hallway constantly stepping back to let the other pass, and because of that you never get past each other. Funny to watch, terrible in a program.
Livelock example: programmers at a door
Imagine two very polite programmers walking towards each other down a narrow hallway. They both arrive at a door at the same time and both decide to yield: step aside to let the other go. Both see the situation hasn't changed, and again try to yield — infinitely. Nobody stands still, everyone moves, but nobody passes through the door.
In a multithreaded program it looks like this: a thread sees a resource is busy, backs off, checks again, yields again, and so on forever.
2. Livelock in practice
Let's implement an analogous situation in code. Suppose we have two resources (for example, two bank accounts), and two threads try to transfer money to each other at the same time, trying not to "deadlock".
class Account
{
public int Balance { get; set; }
public object LockObj { get; } = new object();
}
public class Program
{
static void Transfer(Account from, Account to, int amount)
{
while (true)
{
bool lockedFrom = false;
bool lockedTo = false;
try
{
Monitor.TryEnter(from.LockObj, 100, ref lockedFrom);
Monitor.TryEnter(to.LockObj, 100, ref lockedTo);
if (lockedFrom && lockedTo)
{
from.Balance -= amount;
to.Balance += amount;
break; // Transfer done!
}
}
finally
{
if (lockedFrom) Monitor.Exit(from.LockObj);
if (lockedTo) Monitor.Exit(to.LockObj);
}
// Didn't work? Back off and try again — politely!
Thread.Sleep(1);
}
}
// ... Start two threads with Transfer(A, B, ...); Transfer(B, A, ...);
}
In this example both threads repeatedly try to acquire locks. But if both constantly see the locks occupied and yield, it reduces to “polite” yielding and no money ever gets transferred. Relying on scheduler randomness is a bad strategy for reliable programs. Add pauses and backoff, don't spin tight “empty” loops on Monitor.TryEnter and Thread.Sleep.
Comparison: Livelock vs Deadlock
| Deadlock | Livelock | |
|---|---|---|
| Threads | Stopped, waiting | Actively spinning, waiting |
| Resources | Locked forever | Not explicitly locked |
| CPU | Almost idle | May be 100% busy |
| Fix | Timeouts, lock ordering | Randomness, pauses, backoff |
3. Starvation: when the queue never reaches the resource
Starvation (literally "starvation") is a situation where one or more threads are constantly ignored and never get access to the needed resource because other threads keep getting ahead.
Unlike Deadlock and Livelock, here nobody is blocked forever and nobody yields infinitely — it's just that some threads never get their "slice of the pie".
Illustration: a real-life analogy
Imagine a cafeteria with two queues: "VIP" (for example, an employee-of-the-year cashier) and "everyone else". The VIP line is short, but someone always gets there first. Regular visitors wait half an hour watching VIPs go ahead. That's Starvation!
4. Starvation in C#: practice and debugging
Consider a situation with lock where one thread with higher priority constantly manages to enter the critical section, and another "lags behind":
private static readonly object _locker = new object();
static void Greedy()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Greedy thread grabbed the resource...");
Thread.Sleep(10); // holds the lock longer
}
Thread.Sleep(1);
}
}
static void Poor()
{
while (true)
{
lock (_locker)
{
Console.WriteLine("Poor thread tried to enter...");
}
}
}
If you run both threads, "Greedy" holds the lock for a long time and quickly reacquires it, while "Poor" rarely gets in and effectively "starves" without the resource. In real apps starvation can be less obvious: for example, if one thread has lower priority or is preempted by the OS more often.
Where does starvation occur?
- In low-priority threads.
- With poorly organized task queues (no fair FIFO policy).
- In ReaderWriterLockSlim with default settings: if writers arrive rarely and readers are endless, the writer will starve because readers hold the Read Lock and prevent getting the Write Lock.
5. Why Starvation is not always a bug, but always a problem
Starvation, unlike Deadlock, doesn't stop the program entirely, but the results become unpredictable and unfair: data is processed unevenly, some tasks wait forever, performance degrades. This is especially critical in server apps where everyone should get fair chances.
How to detect and diagnose Livelock and Starvation
- CPU usage spikes, but the task is "stuck" — you might have Livelock.
- Threads seem to run, but some tasks never finish — likely Starvation.
- Logs: if event logs show certain threads or tasks never get access to a resource — definite starvation.
How to avoid Livelock
- Don't rely only on “yielding” non-blocking attempts like Monitor.TryEnter. If it fails, add random pauses (Thread.Sleep(Random.Next(1, 10)))), so threads don't line up timing-wise.
- Don't spin tight short loops trying to grab a resource — otherwise the "back off — try" loop becomes infinite.
- Sometimes change the algorithm: introduce "seniority rights" so only one thread yields and the other doesn't.
How to avoid Starvation
- Watch thread priorities: background or high-priority tasks shouldn't starve others forever.
- Use queues (ConcurrentQueue<T>, channels, task queues) with a fair FIFO policy so tasks are served in order.
- In ReaderWriterLockSlim you can block new readers in favor of writers so a writer eventually gets the Write Lock.
- Use timeouts and logs: if a thread tries to get a lock too long, emit a warning.
- Reduce lock hold time and split up critical sections.
Comparison: Deadlock, Livelock and Starvation
| Scenario | Deadlock | Livelock | Starvation |
|---|---|---|---|
| Threads | Dead stopped | Run but uselessly | Some run, some don't |
| Resource access | No | No | Uneven, sometimes no |
| CPU | Not busy | Busy | Busy, but not by all |
| Critical bug? | Yes | Yes | Sometimes |
GO TO FULL VERSION