Prevention
In this lesson, we discuss techniques to prevent different conditions that can cause a deadlock, from arising.
We'll cover the following
In the previous lesson, we saw four conditions that are needed for a deadlock to occur. Let’s now discuss the strategies to prevent each of these conditions.
Circular wait
Probably the most practical prevention technique (and certainly one that is frequently employed) is to write your locking code such that you never induce a circular wait. The most straightforward way to do that is to provide a total ordering on lock acquisition. For example, if there are only two locks in the system (L1
and L2
), you can prevent deadlock by always acquiring L1
before L2
. Such strict ordering ensures that no cyclical wait arises; hence, no deadlock.
Of course, in more complex systems, more than two locks will exist, and thus total lock ordering may be difficult to achieve (and perhaps is unnecessary anyhow). Thus, a partial ordering can be a useful way to structure lock acquisition so as to avoid deadlock. An excellent real-life example of partial lock ordering can be seen in i_mutex
before i_mmap_rwsem
” and more complex orders such as “i_mmap_rwsem
before private_lock
before swap_lock
before i_pages lock
”.
As you can imagine, both total and partial ordering require careful design of locking strategies and must be constructed with great care. Further, ordering is just a convention, and a sloppy programmer can easily ignore the locking protocol and potentially cause deadlock. Finally, lock ordering requires a deep understanding of the code base, and how various routines are called; just one mistake could result in the
Hold-and-wait
The hold-and-wait requirement for deadlock can be avoided by acquiring all locks at once, atomically. In practice, this could be achieved as follows:
pthread_mutex_lock(prevention); // begin acquisitionpthread_mutex_lock(L1);pthread_mutex_lock(L2);...pthread_mutex_unlock(prevention); // end
By first grabbing the lock prevention
, this code guarantees that no untimely thread switch can occur in the midst of lock acquisition and thus deadlock can once again be avoided. Of course, it requires that any thread grabs a lock any time it first acquires the global prevention lock. For example, if another thread was trying to grab locks L1
and L2
in a different order, it would be OK, because it would be holding the prevention lock while doing so.
Note that the solution is problematic for a number of reasons. As before, the encapsulation works against us: when calling a routine, this approach requires us to know exactly which locks must be held and to acquire them ahead of time. This technique also is likely to decrease concurrency as all locks must be acquired early on (at once) instead of when they are truly needed.
No preemption
Because we generally view locks as held until unlock is called, multiple lock acquisition often gets us into trouble because when waiting for one lock we are holding another. Many thread libraries provide a more flexible set of interfaces to help avoid this situation. Specifically, the routine pthread_mutex_trylock()
either grabs the lock (if it is available) and returns success or returns an error code indicating the lock is held; in the latter case, you can try again later if you want to grab that lock.
Such an interface could be used as follows to build a deadlock-free, ordering-robust lock acquisition protocol:
top:pthread_mutex_lock(L1);if (pthread_mutex_trylock(L2) != 0) {pthread_mutex_unlock(L1);goto top;}
Note that another thread could follow the same protocol but grab the locks in the other order (L2
then L1
) and the program would still be deadlock free. One new problem does arise, however: livelock. It is possible (though perhaps unlikely) that two threads could both be repeatedly attempting this sequence and repeatedly failing to acquire both locks. In this case, both systems are running through this code sequence over and over again (and thus it is not a deadlock), but progress is not being made, hence the name livelock. There are solutions to the livelock problem, too. For example, one could add a random delay before looping back and trying the entire thing over again, thus decreasing the odds of repeated interference among competing threads.
One point about this solution: it skirts around the hard parts of using a try-lock approach. The first problem that would likely exist again arises due to encapsulation. If one of these locks is buried in some routine that is getting called, the jump back to the beginning becomes more complex to implement. If the code had acquired some resources (other than L1
) along the way, it must make sure to carefully release them as well; for example, if after acquiring L1
, the code had allocated some memory, it would have to release that memory upon failure to acquire L2
, before jumping back to the top to try the entire sequence again. However, in limited circumstances (e.g., the Java vector method mentioned earlier), this type of approach could work well.
You might also notice that this approach doesn’t really add preemption (the forcible action of taking a lock away from a thread that owns it) but rather uses the try-lock approach to allow a developer to back out of lock ownership (i.e., preempt their own ownership) in a graceful way. However, it is a practical approach, and thus we include it here, despite its imperfection in this regard.
Mutual exclusion
The final prevention technique would be to avoid the need for mutual exclusion at all. In general, we know this is difficult because the code we wish to run does indeed have critical sections. So what can we do?
As a simple example, let us assume we have a compare-and-swap instruction, which as you may recall is an atomic instruction provided by the hardware that does the following:
int CompareAndSwap(int *address, int expected, int new) {if (*address == expected) {*address = new;return 1; // success}return 0; // failure}
Imagine we now wanted to atomically increment a value by a certain amount, using compare-and-swap. We could do so with the following simple function:
void AtomicIncrement(int *value, int amount) {do {int old = *value;} while (CompareAndSwap(value, old, old + amount) == 0);}
Instead of acquiring a lock, doing the update, and then releasing it, we have instead built an approach that repeatedly tries to update the value to the new amount and uses the compare-and-swap to do so. In this manner, no lock is acquired, and no deadlock can arise (though livelock is still a possibility, and thus a robust solution will be more complex than the simple code snippet above).
Let us consider a slightly more complex example: list insertion. Here is code that inserts at the head of a list:
void insert(int value){node_t *n = malloc(sizeof(node_t));assert(n != NULL);n->value = value;n->next = head;head =n;}
This code performs a simple insertion, but if called by multiple threads at the “same time”, has a race condition. Can you figure out why? Think of what could happen to a list if two concurrent insertions take place, assuming, as always, a malicious scheduling interleaving. Of course, we could solve this by surrounding this code with a lock acquire and release:
void insert (int value){node_t *n = malloc(sizeof(node_t));assert(n != NULL);n->value = value;pthread_mutex_lock(listlock); // begin critical sectionn->next = head;head = n;pthread_mutex_unlock(listlock); // end critical section}
In this solution, we are using
void insert(int value){node_t *n = malloc(sizeof(node_t)); assert(n != NULL);n->value = value;do{n->next = head;} while (CompareAndSwap(&head, n->next, n) == 0);}
The code here updates the next pointer to point to the current head and then tries to swap the newly-created node into position as the new head of the list. However, this will fail if some other thread successfully swapped in a new head in the meanwhile, causing this thread to retry again with the new head.
Of course, building a useful list requires more than just a list insert, and not surprisingly building a list that you can insert into, delete from, and perform lookups on in a lock-free manner is non-trivial.
Get hands-on with 1400+ tech skills courses.