Asynchronous Programming with Python

Deadlocks and starvation

Your browser needs to be JavaScript capable to view this video

Try reloading this page, or reviewing your browser settings

This segment shows you other pitfalls of using locks - deadlocks and starvation.

Keywords

  • Python
  • locks
  • deadlocks
  • starvation
  • protecting shared resources

About this video

Author(s)
Coen de Groot
First online
22 December 2020
DOI
https://doi.org/10.1007/978-1-4842-6582-6_5
Online ISBN
978-1-4842-6582-6
Publisher
Apress
Copyright information
© Coen de Groot 2020

Video Transcript

Always releasing locks is not enough. Locks are tricky things and carry some risks. Let’s imagine a meal for 10 people, but only one fork and one spoon. Diners can only eat when they’ve got the fork and the spoon. In our program, we may have two shared resources, and some functions can only run if they have both.

In our example, we have one function which first picks up the fork as soon as it is available and then the spoon, or waits until it is free. The other function picks up the spoon as soon as it can and then the fork. We create 10 diners, a random collection of fork-first and spoon-first diners, and then tell them all to start eating.

The first diner who starts has a slight advantage. It takes a little while to start the next thread– just enough time for the first diner to grab the spoon and the fork and print the message, “Picked up cutlery, eating.” In the 0.1 seconds it takes for the first diner to complete the meal, all other diners are started as well. And you see the message, “Everyone has started their meal.”

The first diner puts down their cutlery. A fork-first diner picks up the fork and tries to pick up a spoon. However, a spoon-first diner got there first. One diner is holding a fork and waiting for a spoon, whilst another diner is holding a spoon and waiting for a fork. The other diners are also waiting. The only way out is to manually abort the program. The diners are stuck in a deadlock.

In short, we have two or more threads, each holding a lock for one shared resource whilst waiting to access to the other shared resource. Here are some ideas on how to avoid deadlocks. Only lock the section of code which changes a shared resource– no more, no less. If both shared resources are always or usually needed together, it is much safer to protect both of them with a single lock. If you always acquire locks in the same order, you can never end up with two threads holding one lock each and waiting for each other.

If there is still a chance of deadlocks, once you are holding one lock and you’re waiting for a second look, timeout if it takes too long. Release the first lock. Now, wait a brief while to give other threads a chance to grab the lock and start again. If no one wanted the lock, you’ll get it back straight away.

A more subtle problem is starvation. The code doesn’t get stuck like a deadlock. Some threads get work done. However, the way things work out, other threats never get the chance.

Here’s an example of starvation and of using timeouts. We’ll stick with our diners. This time, they’re having soup and only need a spoon. Of course, there’s still only one spoon. Because there’s only one lock, there’s no risk of deadlock.

There are two types of eaters. The determined eater picks up the spoon as soon as it is free or waits until it is free. The impatient eater doesn’t like waiting. They try to pick up the spoon, but if they don’t get it within 0.02 seconds, they walk away from the table and come back after 0.1 seconds to try again.

Once a diner has finished their soup, they increase the progress, so we can see how many determined diners and how many impatient diners have finished. The final loop prints out the progress every 0.2 seconds until all threads are done. What do you think will happen when we run the code?

All the determined eaters go first. The impatient ones keep walking away and keep missing their chance of getting a spoon. One the determined eaters are done, the impatient eaters finally get a chance. Our diners share the same spoon, but there is an imbalance in their behavior.

The determined diners clearly have the best chance of getting a spoon. Fortunately, they didn’t come back for seconds! It will be possible for one process or one type of process to dominate a shared resource and completely block other processes from accessing it.

Imagine five people on a round table with one piece of cutlery between each two diners. They can only pick up both pieces of cutlery at once or wait until they are both available. Even though all diners use the same process, one of them may be really unlucky. Just as the diner to their left puts down their cutlery, the one on the right picks up theirs, and vice versa. That diner may never be able to pick up the cutlery and will starve– or, more likely, just walk away and eat somewhere else.

This is called the Dining Philosophers Problem. In short, be careful how you use locks. Use as few as possible, and keep the count as simple as possible to help you understand what is happening. We will soon see some alternatives, some higher-level abstractions which may help as well.