Last Updated on September 12, 2022
You can perform busy waiting in a thread with a while-loop and an if-condition.
In this tutorial you will discover how to use busy waiting in Python.
Let’s get started.
Need for Busy Waiting
A thread is a thread of execution in a computer program.
Every Python program has at least one thread of execution called the main thread. Both processes and threads are created and managed by the underlying operating system.
Sometimes we may need to create additional threads in our program in order to execute code concurrently.
Python provides the ability to create and manage new threads via the threading module and the threading.Thread class.
You can learn more about Python threads in the guude:
In concurrency programming we may want to check for a specific result or a change repeatedly in a loop. This is called spinning or busy waiting and can be useful in some circumstances.
What is busy waiting and how can we perform busy waiting in Python?
Run loops using all CPUs, download your FREE book to learn how.
What is Busy Waiting
Busy waiting, also called spinning, refers to a thread that repeatedly checks a condition in a loop.
It is referred to as “busy” or “spinning” because the thread continues to execute the same code, such as an if-statement within a while-loop, achieving a wait by executing code (e.g. keeping busy).
- Busy Wait: When a thread “waits” for a condition by repeatedly checking for the condition in a loop.
Busy waiting can be compared to waiting by a blocking call, such as a sleep or a wait.
Busy waiting is typically undesirable in concurrent programming as the tight loop of checking a condition consumes CPU cycles unnecessarily, occupying a CPU core. As such, it is sometimes referred to as an anti-pattern of concurrent programming, a pattern to be avoided.
That being said, there are some occasions where a busy wait is a preferred solution, such as situations where there communication or signals between threads may be missed due to the unpredictable execution times or race conditions. In such cases, a busy wait can provide an alternative to other concurrency primitives.
Additionally, if the busy wait involves checking if a mutual exclusion lock is available, this is referred to as a spinlock.
- Spinlock: A type of busy waiting where a thread is checking and waiting for a mutex lock.
Now that we are familiar with what busy waiting is, let’s look at how we might perform busy waiting in Python.
How to Perform Busy Waiting
We can perform busy waiting in Python with an if-statement within a while-loop.
The while-loop may loop forever and the if-statement will check the desired goal state and break the loop.
For example:
1 2 3 4 5 6 |
... # busy wait while True: # check for the goal state if goal_state(): break |
Here, we simplify the if-statement to a function call, but it could be any condition relevant to the program.
We can see that this tight loop will execute as fast as possible, checking the condition every iteration.
It is sometimes referred to as an anti-pattern because of so much calculation being dedicated to redundant checks of the same condition.
The computation used to perform the loop may be lessened some amount. Two approaches may include:
- Adding a sleep to the busy-wait loop, e.g. a call to time.sleep().
- Adding a blocking wait, if a concurrency primitive like a lock or a condition is being used.
Adding a sleep, even of a fraction of a second, will allow the operating system to context switch to another thread of execution and make progress.
1 2 3 4 5 6 7 8 |
... # busy wait while True: # check condition if goal_state(): break else: sleep(0.1) |
This may be desirable for some applications, if a small gap in time between the condition being True and the thread checking the condition is acceptable.
Alternatively a blocking wait() call can be made on a shared concurrent primitive like a threading.Lock or a threading.Condition can be used. Again, a timeout can be set on the blocking call to allow the thread to periodically check the condition.
This approach can be used in a spinlock to reduce the computational burden waiting for a mutex threading.Lock to become available. It can also be used with a threading.Condition, if a race condition may mean that a notification from another thread may be missed
Like adding a sleep, this is only appropriate if the application can tolerate a small gap in time between the condition being True and the thread noticing the change.
1 2 3 4 5 6 7 8 9 10 11 |
... # busy wait while True: # acquire the condition with condition: # check condition if goal_state(): break else: # block with a timeout condition.wait(timeout=0.1) |
Now that we know how to perform busy waiting in Python, let’s look at some worked examples
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
Example of Race Condition With Wait and Notify
Before we dive in, let’s start with a concurrency programming failure case that can be solved using a busy wait.
An important use case for a busy wait is when one thread is waiting to be notified by another thread, but due to the unpredictable nature of the program it is possible for the waiting thread to miss the notification.
This is a type of race condition. There are many ways to solve this race condition, and a busy wait is one possible solution.
We will develop an example where the main thread puts some data in a list and another thread waits for and then uses the data in the list. A condition is used in an attempt to notify the new thread that the data is available. We will then make the execution time of the main thread and the new thread unpredictable and show that sometimes the wait-notify with the condition works, and sometimes it fails due to a race condition.
First, we can define a function named task() to execute in a new thread. The function will take an instance of the shared threading.Condition instance and the list in which data will be placed.
1 2 3 |
# target function def task(condition, data): # ... |
The function will first block for a fraction of a second to give some unpredictability regarding when exactly it will check if the data has arrived.
1 2 3 |
... # block for a moment sleep(random()) |
Next, the thread will acquire a lock on the condition via the context manager, and then wait for the main thread to notify it that the data has arrived.
1 2 3 4 5 |
... # wait for data with condition: print('.thread waiting on condition') condition.wait() |
The call to wait() on the threading.Thread will block forever until the notification signal is received. It also releases the lock on the condition until the thread is notified.
Finally, the function will (optimistically) report the data that was received.
1 2 |
... print(f'Thread got data: {data}') |
Tying this together, the complete task() function to be executed in a new thread is listed below.
1 2 3 4 5 6 7 8 9 |
# target function def task(condition, data): # block for a moment sleep(random()) # wait for data with condition: print('.thread waiting on condition') condition.wait() print(f'Thread got data: {data}') |
Next, in the main thread we can create a new threading.Condition to be shared between the main thread and the new thread, and a list in which data will be placed.
1 2 3 4 5 |
... # create the condition condition = Condition() # create the data storage data = list() |
We can then create a new threading.Thread instance and configure it to execute our task() function and pass the threading.Condition and the list in which data will arrive. Once created the new thread can be started.
1 2 3 4 5 |
... # create a new thread thread = Thread(target=task, args=(condition,data)) # start the new thread thread.start() |
Next, we can add unpredictability to the execution timing of the main thread by having it block for a random fraction of a second.
1 2 3 |
... # block for a moment sleep(random()) |
Finally, the main thread will acquire the condition, add some data and notify the waiting thread.
1 2 3 4 5 6 7 8 |
... # acquire the condition with condition: # store data data.append('We did it!') # notify waiting threads print('Main is notifying') condition.notify() |
A careful review of this code will show that there is a race condition.
Specifically, that the new thread may call wait() after the main thread calls notify() on the threading.Condition. If this occurs, the new thread will wait forever. This may happen sometimes, and other times it may be the case that the new thread calls wait() then the main thread calls notify(), in which case the expected outcome of the new thread receiving the data occurs and the program exits normally.
Tying this together, the complete example of a race condition with wait() and notify() is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# SuperFastPython.com # example of a race condition with wait and notify on a condition from time import sleep from random import random from threading import Thread from threading import Condition # target function def task(condition, data): # block for a moment sleep(random()) # wait for data with condition: print('.thread waiting on condition') condition.wait() print(f'Thread got data: {data}') # create the condition condition = Condition() # create the data storage data = list() # create a new thread thread = Thread(target=task, args=(condition,data)) # start the new thread thread.start() # block for a moment sleep(random()) # acquire the condition with condition: # store data data.append('We did it!') # notify waiting threads print('Main is notifying') condition.notify() |
Running the example may result in a different outcome each time given the use of random numbers. Run the example a number of times.
Let’s look at a normal outcome first, then a race condition outcome.
The main thread creates the condition, then creates a new thread which starts executing. The new thread calls the task() function which acquires the condition and begins waiting on the main thread for the data to arrive.
The main thread blocks for a moment, wakes-up then acquires the condition, adds the data and then notifies the new thread that data has arrived.
Finally, the new thread wakes up, given the notification from the main thread, then reports the data.
1 2 3 |
.thread waiting on condition Main is notifying Thread got data: ['We did it!'] |
This was the normal case, which will occur some of the time when the code is run.
Other times we may get a race condition.
The main thread creates the condition, the new thread, and starts the thread as before. The new thread begins executing a block on the call to sleep() for a while.
The main thread carries on, acquires the condition, adds the data, then notifies any threads waiting on the condition and is done.
The new thread unblocks from the call to sleep(), acquires the condition and waits on the condition to be notified that data has arrived.
In this case a race condition has occurred and it is never notified. The thread waits forever and the program must be killed.
1 2 |
Main is notifying .thread waiting on condition |
Next, let’s look at how we might fix this race condition by adding a busy wait.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of a Busy Waiting
We can update the example from the previous section to add a busy wait that will address the race condition with wait and notify on the condition.
A busy wait can be added to the task() function. This involves adding a while-loop that loops forever and an if-statement that checks for the desired goal state. In this case, the goal state is whether or not data has been placed in the list.
Once data has arrived, we can break out of the while-loop and continue on.
For example:
1 2 3 4 5 6 7 8 9 10 11 |
# target function def task(condition, data): # block for a moment sleep(random()) # wait for data while True: print('.thread waiting on condition') # check the data if len(data) > 0: break print(f'Thread got data: {data}') |
This would probably work.
Nevertheless, there is a new race condition between the main thread calling data.append() to add the data and the new thread checking the length of the list in the if-statement.
This race condition can be easily avoided by treating the shared threading.Condition as a mutex lock.
Both the main thread and the new thread must acquire the condition before interacting with the data list. This will remove any race condition with the data list as only one thread can acquire the condition at a time.
The updated task() function with a busy wait and no race condition on the data list is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 |
# target function def task(condition, data): # block for a moment sleep(random()) # wait for data while True: with condition: print('.thread waiting on condition') # check the data if len(data) > 0: break print(f'Thread got data: {data}') |
The important change here is that the new thread no longer waits on the condition, but instead waits on the goal state directly.
Another important point is that we acquire the threading.Condition within the while loop, so that we can release it each iteration of the loop, allowing the main thread to acquire it. If the loop was nested within the acquisition of the threading.Condition then the main loop may never be able to acquire the condition and add the data.
Tying this together, the complete example of the busy wait is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# SuperFastPython.com # example of a busy wait for a condition from time import sleep from random import random from threading import Thread from threading import Condition # target function def task(condition, data): # block for a moment sleep(random()) # wait for data while True: with condition: print('.thread waiting on condition') # check the data if len(data) > 0: break print(f'Thread got data: {data}') # create the condition condition = Condition() # create the data storage data = list() # create a new thread thread = Thread(target=task, args=(condition,data)) # start the new thread thread.start() # block for a moment sleep(random()) # acquire the condition with condition: # store data data.append('We did it!') # notify waiting threads print('Main is notifying all') condition.notify_all() |
Running the example first creates the condition and data list, then creates and starts the new thread.
The new thread blocks for a fraction of a second then loops checking the state of the data list, reporting lots of messages about waiting.
The main thread blocks for a fraction of a second, then acquires the condition and adds the data.
The new thread checks the data list on one of its many iterations of the loop, sees the change to the data list and then breaks the loop reporting the result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition Main is notifying all .thread waiting on condition Thread got data: ['We did it!'] |
The downside of this busy loop is that it consumes many CPU cycles doing essentially the same redundant check of the data list.
Next, let’s see if we can save some CPU cycles by adding a sleep.
Example of Busy Waiting With a Sleep
Although a busy-wait can remove a race condition, it can be costly in terms of computation.
If the application allows it, we can add a sleep within the busy wait loop. This will allow the operating system to context switch to another thread, possibly freeing up a CPU core, although may add some delay between the goal state being triggered and the new thread noticing.
This can be achieved by adding a call to sleep() with some small fraction of a second, such as 100 or 200 milliseconds (e.g. 0.1 or 0.2 seconds). This does not sound like a lot in “user time”, but in “CPU time”, it is a long time.
For example:
1 2 3 4 5 6 7 8 9 10 |
... # wait for data while True: with condition: print('.thread waiting on condition') # check the data if len(data) > 0: break # wait for the data sleep(0.2) |
We can still call this a busy wait or busy waiting, although technically the thread is blocking some of the time. The code is still spinning, checking an if-statement in a loop instead of blocking until the goal state is met.
Importantly, the sleep() function is called after we release the threading.Condition. The reason is that the threading.Condition would not be released if the thread blocks on a sleep call, preventing the main thread from acquiring the condition and doing its work.
The updated version of the task() function with this change is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# target function def task(condition, data): # block for a moment sleep(random()) # wait for data while True: with condition: print('.thread waiting on condition') # check the data if len(data) > 0: break # wait for the data sleep(0.2) print(f'Thread got data: {data}') |
Tying this together, the complete example of a busy wait with a sleep is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# SuperFastPython.com # example of a busy wait for a condition with a sleep from time import sleep from random import random from threading import Thread from threading import Condition # target function def task(condition, data): # block for a moment sleep(random()) # wait for data while True: with condition: print('.thread waiting on condition') # check the data if len(data) > 0: break # wait for the data sleep(0.2) print(f'Thread got data: {data}') # create the condition condition = Condition() # create the data storage data = list() # create a new thread thread = Thread(target=task, args=(condition,data)) # start the new thread thread.start() # block for a moment sleep(random()) # acquire the condition with condition: # store data data.append('We did it!') # notify waiting threads print('Main is notifying all') condition.notify_all() |
Running the example creates and starts the new thread as before.
The new thread blocks for a fraction of a second then begins the busy wait loop. Each loop, the thread acquires the condition, checks the data list protected by the condition, and if not populated, it sleeps for a fraction of a second.
The main thread blocks for a moment, then acquires the condition and populates the data. Although the main thread notifies waiting threads, there are no threads listening.
Finally, the main thread notices the change and reports the arrival of the data.
The sleep means we achieve many fewer iterations of the busy wait loop before the data list is populated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
.thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition .thread waiting on condition Main is notifying all .thread waiting on condition Thread got data: ['We did it!'] |
Next, let’s look at how we might incorporate a blocking wait to the busy wait loop.
Example of Busy Waiting With A Wait
We can update the race condition example so that the busy wait loop spins as before, but also waits to be notified by the main thread.
This may be a more efficient solution as it allows for the race condition by iterating the check in a loop, but also allows the thread to be notified by the main thread at the instant the data arrives.
This can be achieved by spending most of the time blocking on a call to the wait() function on the threading.Condition, although with a timeout. Once the timeout elapses (without being notified), the thread will spend a tiny fraction of time checking the condition manually.
This is a useful pattern if a thread may be waiting on many notifications over time, or if a goal state may be achieved with or without a notification from another thread.
You may recall that we cannot wait on a threading.Condition unless we have acquired the condition, therefore the call to wait() must happen within the context manager block of the condition.
In fact, we can nest the entire busy wait loop under the threading.Condition context manager as the condition will be made available to other threads while the thread holding the condition calls wait().
The updated busy wait loop with a blocking wait call is listed below. Note the use of a timeout in the call to wait, ensuring that the thread does not block for more than 200 milliseconds.
1 2 3 4 5 6 7 8 9 10 11 |
... # wait for data with condition: while True: print('.thread waiting on condition') # check the data if len(data) > 0: break else: # wait to be notified with a timeout condition.wait(timeout=0.2) |
This change allows the thread to respond to data changes directly, and/or to notification signals.
It is still a busy wait as it spins checking the condition each loop, slowed down by blocking wait calls on a shared concurrency primitive known to signal the desired goal state.
The updated task() function is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# target function def task(condition, data): # block for a moment sleep(random()) # wait for data with condition: while True: print('.thread waiting on condition') # check the data if len(data) > 0: break else: # wait to be notified with a timeout condition.wait(timeout=0.2) print(f'Thread got data: {data}') |
Tying this together, the complete example of a busy wait with a blocking wait call is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# SuperFastPython.com # example of a busy wait for a condition with a blocking wait from time import sleep from random import random from threading import Thread from threading import Condition # target function def task(condition, data): # block for a moment sleep(random()) # wait for data with condition: while True: print('.thread waiting on condition') # check the data if len(data) > 0: break else: # wait to be notified with a timeout condition.wait(timeout=0.2) print(f'Thread got data: {data}') # create the condition condition = Condition() # create the data storage data = list() # create a new thread thread = Thread(target=task, args=(condition,data)) # start the new thread thread.start() # block for a moment sleep(random()) # acquire the condition with condition: # store data data.append('We did it!') # notify waiting threads print('Main is notifying all') condition.notify_all() |
Running the example starts the new thread and before.
The new thread acquires the condition then starts a busy wait loop of checking the data list and waiting on the condition.
Each run may produce a different result.
In this case, the thread completes two iterations of the busy wait loop then the main thread acquires the condition, adds the data, and notifies the new threads that reports the result.
1 2 3 4 5 |
.thread checking the event... .thread checking the event... Main setting the event... .thread checking the event... Thread noticed the event is set...finally |
Try running the example a few times.
In this second run, the main thread acquires the condition, stores the data and notifies waiting threads.
The new thread has not yet started waiting on the condition, but notices the change directly and reports the result.
1 2 3 |
Main is notifying all .thread waiting on condition Thread got data: ['We did it!'] |
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know how to use a busy wait in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Harley-Davidson on Unsplash
Do you have any questions?