Last Updated on September 12, 2022
You can create a spinlock by using a busy wait loop when attempting to acquire a threading.Lock.
In this tutorial you will discover how to use a spinlock in Python.
Let’s get started.
Need for a Spinlock
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 perform actions while waiting for a lock to become available. Waiting for a mutex lock in a loop is called a spinlock.
What is a spinlock and how can we make use of it in Python?
Run loops using all CPUs, download your FREE book to learn how.
What is a Spinlock
A spinlock is a busy wait for a mutual exclusion (mutex) lock.
Busy waiting, also called spinning, refers to a thread that repeatedly checks a condition.
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 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.
If you are new to busy waiting, you can learn more here:
Nevertheless, a busy wait can be used to address race conditions in concurrency programming.
A spinlock is a type of lock where a thread must perform a busy wait if it is already locked.
- Spinlock: Using a busy wait loop to acquire a lock.
Spinlocks are typically implemented at a low-level, such as in the underlying operating system. Nevertheless, we can implement a spinlock at a high-level in our program as a concurrency primitive.
A spinlock may be a desirable concurrency primitive that gives fine grained control over both how long a thread may wait for a lock (e.g. loop iterations or wait time) and what the thread is able to do while waiting for the lock.
This latter point of performing actions while waiting for a lock is the most desirable property of a spinlock, as this might include a suite of application specific tasks such as checking application state or logging status.
Now that we know what a spinlock is, let’s look at how we can create one in Python.
How to Create a Spinlock
We can create a spinlock using a threading.Lock in Python.
This can be achieved by using a mutex lock and having a thread perform a busy wait look in order to acquire it.
The busy wait loop in a spinlock may look as follows:
1 2 3 4 5 6 7 8 9 |
... # example of a busy wait loop for a spinlock while True: # try and get the lock acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # stop waiting break |
We can see that the busy loop will loop forever until the lock is acquired.
Each iteration of the loop, we attempt to acquire the lock without blocking. If acquired, the loop will then exit.
Later, the lock must be released.
If the critical section protected by the lock is short enough, it can be called directly from within the busy loop, after the lock is acquired.
We can contrast the above busy wait loop of the spinlock with the alternate of simply blocking until the lock becomes available.
1 2 3 |
... # block until the lock can be acquired lock.acquire() |
In this case, the waiting thread is unable to perform any other action while waiting on the lock.
A limitation of the spinlock is the increased computational burden of executing a tight loop repeatedly.
The computational burden of the busy wait loop can be lessened by adding a blocking sleep, e.g. a call to time.sleep() for a fraction of a second.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 |
... # example of a busy wait loop for a spinlock with a sleep while True: # try and get the lock acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # stop waiting break else: # sleep for a moment sleep(0.1) |
Alternatively, the attempt to acquire the lock each loop iteration could use a short blocking wait with a timeout of a fraction of a second. The acquire() function on the threading.Lock takes a “timeout” argument that could be set to 100 milliseconds (e.g. 0.1 seconds).
1 2 3 4 5 6 7 8 9 |
... # example of a busy wait loop for a spinlock with a wait while True: # try and get the lock with a timeout acquired = lock.acquire(timeout=0.1) # check if we got the lock if acquired: # stop waiting break |
Both approaches would reduce the number of iterations of the busy loop, causing most of the execution time spent sleeping or blocking. This would allow the operating system to context switch the thread and perhaps free-up a CPU core.
The downside of adding a sleep or a blocking wait, is that it may introduce a delay between the locking becoming available and the thread noticing this fact and acquiring it. The tolerance for such a delay will be application dependent.
A further improvement would be to give-up attempting to acquire the lock after a fixed time limit or number of iterations of the loop.
Now that we know how to implement a spinlock 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 a Spinlock
We can explore how to create a spinlock to acquire a mutex lock.
In this example, we will set up the situation where the main thread creates a lock then creates and starts a new thread that attempts to acquire the lock using a spinlock. The new thread will introduce a delay before attempting to acquire the lock. In this delay, the main thread itself will acquire and hold the lock, causing the new thread to execute a busy wait loop of the spinlock for many iterations before finally acquiring the lock.
First, we must define a function that implements the busy wait loop to implement the spinlock.
We will name the function task() and it will take the instance of the shared mutex lock as an argument.
1 2 3 |
# target function def task(lock): # ... |
The function must be delayed from executing in order to give the main thread a chance to acquire the lock first and allow us to demonstrate the spinlock in action.
1 2 3 |
... # block for a moment sleep(0.5) |
Next, we can implement the busy wait loop that will loop forever.
1 2 3 |
# busy wait to get the lock while True: # ... |
Within an iteration, the thread must first attempt to acquire the lock without blocking.
1 2 3 4 |
... # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=False) |
If the lock was acquired, we can then execute our critical section, release the lock again and then break the busy loop.
In this case, our critical section will be simply to report a message.
1 2 3 4 5 6 7 8 9 |
... # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break |
Tying this together, the complete task() function with the spinlock busy wait loop is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break |
In the main thread, we can first create an instance of the shared threading.Lock instance.
1 2 3 |
... # create the mutex lock lock = Lock() |
We can then create a new threading.Thread instance and configure it to execute our task() function with the single lock argument via the “target” and “args” arguments to the class constructor.
1 2 3 |
... # create a new thread thread = Thread(target=task, args=(lock,)) |
The new thread can then be started.
1 2 3 |
... # start the new thread thread.start() |
Finally, the main thread can acquire the lock and hold it for an extended period. This will force the new thread to wait via the busy wait loop for a moment before the lock is made available.
1 2 3 4 5 6 7 |
... # acquire the lock print('Main acquiring lock') with lock: # block for a moment sleep(5) print('Main all done') |
Tying this together, the complete example of a spinlock 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 |
# SuperFastPython.com # example of a spinlock from time import sleep from threading import Thread from threading import Lock # target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break # create the mutex lock lock = Lock() # create a new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # acquire the lock print('Main acquiring lock') with lock: # block for a moment sleep(5) print('Main all done') |
Running the example first creates the lock then the new thread which is started.
The new thread starts executing and blocks for a moment with a sleep.
The main thread acquires the lock and holds it for five seconds. Meanwhile, the new thread begins executing the busy wait loop of the spin lock and attempts to acquire the lock each iteration.
The main thread releases the lock. The new thread notices that the lock is available, acquires it, executes its critical section printing a message, then releases the lock again breaking the busy wait loop.
The computational burden of the spinlock is made clear by five seconds worth of the message “.thread trying to acquire the lock” that is printed to the terminal, once per iteration of the busy wait loop.
A truncated version of the output is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Main acquiring lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock ... .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock Main all done .thread trying to acquire the lock Thread got the lock...finally |
Next, let’s explore how we might reduce the computation burden of the spinlock by introducing sleep.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Example of a Spinlock With a Sleep
We can explore how to reduce the computational burden of the spinlock by adding a sleep.
This can be achieved by making a call to time.sleep() each iteration of the busy wait loop when we know the lock has not been acquired.
The sleep time can be chosen based on the specifics of the application and the trade-off between and how large a delay can be tolerated between the lock being available and the thread acquiring it versus how much CPU effort should be expended in the loop.
We will sleep for 500 milliseconds which will reduce the number of iterations of the loop to about 2 hertz, e.g. two iterations of the loop per second.
1 2 3 4 5 6 7 8 9 10 11 |
... # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break else: sleep(0.5) |
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 15 16 17 18 19 |
# target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break else: sleep(0.5) |
Tying this together, the complete example of a spinlock 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 |
# SuperFastPython.com # example of a spin lock with a sleep from time import sleep from threading import Thread from threading import Lock # target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=False) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break else: sleep(0.5) # create the mutex lock lock = Lock() # create a new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # acquire the lock print('Main acquiring lock') with lock: # block for a moment sleep(5) print('Main all done') |
Running the example creates the lock, then creates and starts the new thread as before.
By design, the main thread acquires the lock which forces the new thread to execute the busy wait loop of the spinlock.
In this case, the computational burden of the busy wait loop is lessened, executing only two iterations per second, spending most of its time blocked in a call to sleep.
After 10 iterations of the loop, the lock is made available and acquired by the new thread, executes its critical section and then terminates.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Main acquiring lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock Main all done .thread trying to acquire the lock Thread got the lock...finally |
Next, let’s take a look at how we can introduce a blocking wait into the busy wait loop of the spinlock.
Example of a Spinlock With a Blocking Wait
We can also reduce the computational burden of the spinlock by using a blocking wait.
This can be achieved by configuring the attempt to acquire the lock each iteration to block for a fraction of a second.
1 2 3 |
... # try and get the lock acquired = lock.acquire(blocking=True, timeout=0.5) |
The benefit of using a blocking wait in the lock itself, is that there will not be a delay between the lock being available and the new thread acquiring it as there is with using a sleep.
This means the loop is both more computationally efficient and responsive, while giving fine grained control over how often to execute the busy wait loop in order to perform other required tasks while waiting.
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 15 16 17 |
# target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=True, timeout=0.5) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break |
Tying this together, the complete example of a spinlock with a blocking 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 |
# SuperFastPython.com # example of a spin lock with a blocking wait from time import sleep from threading import Thread from threading import Lock # target function def task(lock): # block for a moment sleep(0.5) # busy wait to get the lock while True: # try and get the lock print('.thread trying to acquire the lock') acquired = lock.acquire(blocking=True, timeout=0.5) # check if we got the lock if acquired: # report a message print("Thread got the lock...finally") # release the lock lock.release() # stop waiting break # create the mutex lock lock = Lock() # create a new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # acquire the lock print('Main acquiring lock') with lock: # block for a moment sleep(5) print('Main all done') |
Running the example creates the lock, and starts the new thread.
The main thread acquires the lock, forcing the new thread to execute its busy wait loop.
The new thread executes the loop two times per second much like the previous sleep example, although blocks on the lock itself. This means that most of the busy wait loop time is spent waiting on the lock, which once released will notify the waiting thread so it can acquire it.
The main thread releases the lock, the new thread acquires it and executes its critical section.
1 2 3 4 5 6 7 8 9 10 11 12 |
Main acquiring lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock .thread trying to acquire the lock Main all done Thread got the lock...finally |
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 spinlock 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?