Dead(b)locks
The key thing to recognize is that deadlock doesn't arise from a locking cycle exclusively. Any blocking (or, waiting) cycle will do. So a complete definition of deadlock could be put something like this:
deadlock, n.: When N concurrent tasks enter a cycle where each one is blocked waiting for the next.
Clearly, blocking operations include all kinds of blocking synchronization: acquiring a lock (of course); waiting for a semaphore or condition variable to be signaled; waiting to join with another thread or task...But it also includes any other kind of call that waits for a result, including:
- Acquiring exclusive access to any resource (notably I/O, such as opening a file for writing).
- Waiting for a future or write-once variable to be available (the result of an asynchronous task).
- Waiting for any other kind of message to arrive from elsewhere, whether in-process (waiting for a queue container to become nonempty, waiting for a message to be placed into a GUI message queue) or out-of-process or out-of-box (performing a blocking receive call on a TCP socket, for instance).
- Anything else that waits for some other condition to be satisfied.
Complex Combinations
For a more complex example, consider Figure 1. There are four things going on:
- Thread 1 is a GUI window message handler that's in the middle of processing a message. As part of that processing, it needs to use some shared state and so wants to take a lock on mutex mutwhich is currently held by Thread 4.
- Thread 4 is doing something with the same shared state and so has acquired mut already, and is just waiting to receive a message from a message queuea message that is yet to be sent from Thread 2. (Doing communication inside a critical section is problematic and should be avoided where possible. See [2] for more about why to avoid doing communication while holding a lock.)
- Thread 2 hasn't got around to sending that message yet, because it's waiting to open a file for writing, which is an exclusive modea file currently opened for writing and appending on Thread 3.
- Thread 3 is working with the file, and posts a message to the window, which is a synchronous call that blocks to wait for the message to be handledbut Thread 1 is already handling a message and so can't pump and handle this one.
Finally, for extra fun and just to emphasize the point that any kind of blocking operation will do, consider that deadlock can involve a humanyou! For example:
// Thread 1 (running in the CPU hardware) mut.lock(); char = ReadKeystroke(); // blocks // Task 2 (running in your brain's wetware) Wait for the prompt to appear. // blocks OK, now start typing. // Thread 3 (back in the hardware again) mut1.lock(); // blocks Print ( "Press 'Any' key to continue..." );
Here, Thread 1 can't make progress because it's blocked waiting for you to press a key. You haven't pressed a key because you're still waiting for a prompt. Thread 3 can't print the prompt until it returns from being blocked waiting to lock a mutex...held by Thread 1. Around and around we go.