Last Updated on September 12, 2022
You can use a mutual exclusion (mutex) lock in Python via the threading.Lock class.
In this tutorial you will discover how to use the threading.Lock class to create and use mutex locks.
Let’s get started.
Need for a Mutual Exclusion Lock
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.
We can create and manage new Python threads via the threading module and the threading.Thread class.
When writing concurrent programs we may need to share data or resources between threads, which typically must be protected with a mutual exclusion lock.
What is a mutual exclusion lock and how can we use it in Python?
Run loops using all CPUs, download your FREE book to learn how.
What is a Mutual Exclusion Lock
A mutual exclusion lock or mutex lock is a synchronization primitive intended to prevent a race condition.
A race condition is a concurrency failure case when two threads run the same code and access or update the same resource (e.g. data variables, stream, etc.) leaving the resource in an unknown and inconsistent state.
Race conditions often result in unexpected behavior of a program and/or corrupt data.
These sensitive parts of code that can be executed by multiple threads concurrently and may result in race conditions are called critical sections. A critical section may refer to a single block of code, but it also refers to multiple accesses to the same data variable or resource from multiple functions.
The most commonly used mechanism for ensuring mutual exclusion is a mutual exclusion lock or mutex, or simply lock. A mutex is a special type of object that has support in the underlying hardware. The basic idea is that each critical section is protected by a lock.
— Page 53, An Introduction to Parallel Programming, 2020.
A mutex lock can be used to ensure that only one thread at a time executes a critical section of code at a time, while all other threads trying to execute the same code must wait until the currently executing thread is finished with the critical section and releases the lock.
While one thread “owns” the lock—that is, has returned from a call to the lock function but hasn’t yet called the unlock function—any other thread attempting to execute the code in the critical section will wait in its call to the lock function.
— Page 53, An Introduction to Parallel Programming, 2020.
Each thread must attempt to acquire the lock at the beginning of the critical section. If the lock has not been obtained, then a thread will acquire it and other threads must wait until the thread that acquired the lock releases it.
Before a thread can execute the code in the critical section, it must “obtain” the mutex by calling a mutex function, and when it’s done executing the code in the critical section, it should “relinquish” the mutex by calling an unlock function.
— Page 53, An Introduction to Parallel Programming, 2020.
If the lock has not been acquired, we might refer to it as being in the “unlocked” state. Whereas if the lock has been acquired, we might refer to it as being in the “locked” state.
- Unlocked: The lock has not been acquired and can be acquired by the next thread that makes an attempt.
- Locked: The lock has been acquired by one thread and any thread that makes an attempt to acquire it must wait until it is released.
Locks are created in the unlocked state.
Now that we know what a mutex lock is, let’s take a look at how we can use it in Python.
How to Use the threading.Lock Class
Python provides a mutual exclusion lock via the threading.Lock class.
An instance of the lock can be created and then acquired by threads before accessing a critical section, and released after the critical section.
For example:
1 2 3 4 5 6 7 8 |
... # create a lock lock = Lock() # acquire the lock lock.acquire() # ... # release the lock lock.release() |
Only one thread can have the lock at any time. If a thread does not release an acquired lock, it cannot be acquired again.
The thread attempting to acquire the lock will block until the lock is acquired, such as if another thread currently holds the lock then releases it.
A primitive lock is in one of two states, “locked” or “unlocked”. It is created in the unlocked state.
— LOCK OBJECTS, THREADING – THREAD-BASED PARALLELISM.
We can attempt to acquire the lock without blocking by setting the “blocking” argument to False. If the lock cannot be acquired, a value of False is returned.
1 2 3 |
... # acquire the lock without blocking lock.acquire(blocking=false) |
We can also attempt to acquire the lock with a timeout, that will wait the set number of seconds to acquire the lock before giving up. If the lock cannot be acquired, a value of False is returned.
1 2 3 |
... # acquire the lock with a timeout lock.acquire(timeout=10) |
We can also use the lock via the context manager protocol via the with statement, allowing the critical section to be a block within the usage of the lock and for the lock to be released automatically once the block has completed.
For example:
1 2 3 4 5 6 |
... # create a lock lock = Lock() # acquire the lock with lock: # ... |
This is the preferred usage as it makes it clear where the protected code begins and ends, and ensures that the lock is always released, even if there is an exception or error within the critical section.
We can also check if the lock is currently acquired by a thread via the locked() function.
1 2 3 4 |
... # check if a lock is currently acquired if lock.locked(): # ... |
Now that we know how to use the threading.Lock class, let’s look at a worked example.
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 Using a Mutual Exclusion Lock
We can develop an example to demonstrate how to use the mutex lock.
First, we can define a target task function that takes a lock as an argument and uses the lock to protect a critical section. In this case, the critical section involves reporting a message and blocking for a fraction of a second.
1 2 3 4 5 6 |
# work function def task(lock, identifier, value): # acquire the lock with lock: print(f'>thread {identifier} got the lock, sleeping for {value}') sleep(value) |
We can then create one instance of the threading.Lock shared among the threads, and pass it to each thread that we intend to execute the target task function.
1 2 3 4 5 6 7 |
... # create a shared lock lock = Lock() # start a few threads that attempt to execute the same critical section for i in range(10): # start a thread Thread(target=task, args=(lock, i, random())).start() |
Tying this together, the complete example of using a lock is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# SuperFastPython.com # example of a mutual exclusion (mutex) lock from time import sleep from random import random from threading import Thread from threading import Lock # work function def task(lock, identifier, value): # acquire the lock with lock: print(f'>thread {identifier} got the lock, sleeping for {value}') sleep(value) # create a shared lock lock = Lock() # start a few threads that attempt to execute the same critical section for i in range(10): # start a thread Thread(target=task, args=(lock, i, random())).start() # wait for all threads to finish... |
Running the example starts ten threads that all execute a custom target function.
Each thread attempts to acquire the lock, and once they do, they report a message including their id and how long they will sleep before releasing the lock.
Your specific results may vary given the use of random numbers.
1 2 3 4 5 6 7 8 9 10 |
>thread 0 got the lock, sleeping for 0.8859193801237439 >thread 1 got the lock, sleeping for 0.02868415293867832 >thread 2 got the lock, sleeping for 0.04469783674319383 >thread 3 got the lock, sleeping for 0.20456291750962474 >thread 4 got the lock, sleeping for 0.3689208984892195 >thread 5 got the lock, sleeping for 0.21105944750222927 >thread 6 got the lock, sleeping for 0.052093068060339864 >thread 7 got the lock, sleeping for 0.871251970586552 >thread 8 got the lock, sleeping for 0.932718580790764 >thread 9 got the lock, sleeping for 0.9514093969897454 |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Common Questions
This section lists common questions about the threading.Lock mutex in Python.
Do We Need To Protect Critical Sections In Light of the GIL?
Yes.
The current version of the Python interpreter implements a global interpreter lock (GIL) which ensures that only one thread can execute at a time within a Python process.
After learning about the global interpreter lock (GIL), many new Python programmers assume they can forgo using mutual-exclusion locks (also called mutexes) in their code altogether. If the GIL is already preventing Python threads from running on multiple CPU cores in parallel, it must also act as a lock for a program’s data structures, right? Some testing on types like lists and dictionaries may even show that this assumption appears to hold. But beware, this is not truly the case.
— Page 235, Effective Python, 2019.
Nevertheless, it is possible for the operating system to context switch between threads while a thread is in the middle of updating a variable. This means another thread attempting to access or update the same variable may access a corrupt version of the data (race condition).
Critical sections must be protected in Python, even in light of the GIL.
Why Not Use a Boolean Variable?
Reading and writing a boolean variable is an atomic operation in the Python interpreter, and only one thread can execute at one time given the global interpreter lock in the Python interpreter. Therefore, the same effect could be achieved using a single boolean variable.
I believe this is a true statement at the time of writing, but only with the current version of the Python interpreter.
This behavior might change in later versions of Python and may be different with third-party Python interpreters that do not implement a global interpreter lock.
I recommend implementing correct synchronization primitives like the mutex to protect critical sections of code regardless of the GIL.
What If We Don’t Release The Lock?
Not releasing the lock is a concurrency bug.
If the lock is not released by a thread that acquired it, then it can not be acquired again and the code protected by the critical section cannot be executed again.
What If We Ignore The Lock?
Ignoring the lock is a concurrency bug.
You may write code so that some threads may obey the lock and some may not. This will likely result in a race condition and defeat the purposes of having a lock in the first place.
The lock only protects the critical section (data, resources, etc.) if it is enforced in all accesses to the critical section.
Is the threading.Lock Reentrant?
No.
A reentrant lock is a lock that if held by one thread can be acquired again by the same thread.
The threading.Thread lock is not reentrant, meaning if a thread acquires the lock and attempts to acquire it again in another part of the code, that it will block, resulting in a deadlock bug (a concurrency failure condition).
Reentrant mutual exclusion locks are available in the threading.RLock class.
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 mutex lock 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?