Last Updated on September 12, 2022
You can lock a function by using a threading.Lock.
In this tutorial you will discover how to protect a function from race conditions in Python.
Let’s get started.
Need to Lock a Function
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 guide:
In concurrent programming, we may need to lock a function to protect it from being called concurrently from multiple threads.
How can we lock a function in Python?
Run loops using all CPUs, download your FREE book to learn how.
How to Lock a Function
A function can be locked using a mutual exclusion (mutex) lock.
Mutex Lock
A mutual exclusion lock or mutex lock is a synchronization primitive intended to prevent a race condition.
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.
Python provides a mutual exclusion lock via the threading.Lock class.
First, an instance of the lock must be created.
1 2 3 |
... # create a lock lock = Lock() |
The lock can then be used to protect critical sections of code that require serialized or mutually exclusive access.
This can be achieved by first acquiring the lock by calling the acquire() function before accessing a critical section, then releasing the lock by calling release() after the critical section.
For example:
1 2 3 4 5 6 7 |
... # acquire the lock lock.acquire() # critical section # ... # release the lock lock.release() |
Only one thread can have the lock at any time.
If the lock is being held, another thread attempting to acquire the lock will block until the lock is available.
We can also use the lock via the context manager interface.
This will allow 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 |
... # acquire the lock with lock: # ... |
The context manager is the preferred approach to using the lock as it guarantees the lock is always released, even if an unexpected Error or Exception is raised.
You can learn more about mutex locks in the tutorial:
Lock a Function
There are two ways to lock a function.
They are:
- Lock is acquired and released by the caller of the function.
- Lock is acquired and released within the function.
The choice of approach depends on the details of your application.
For example, if you are able to change the target function, then perhaps managing the lock within the function is preferred.
Alternatively if the function is only called one or a few times, or you do not have control over the target function, then controlling the lock in the caller of the function is preferred.
Let’s take a closer look at each approach.
Lock Function In Caller
A function may be locked by the caller of the function.
The caller first acquires the lock, then calls the function, then releases the lock once the function returns.
- Acquire the lock.
- Call function.
- Release lock.
This can be achieved by using the lock via the context manager interface on the threading.Lock.
For example:
1 2 3 4 5 |
... # acquire the lock with lock: # call the function work() |
A benefit of this approach is that the function itself does not require any change.
A downside of this approach is that all calls to the function must be updated to make use of the lock, and that all callers must share the same lock instance.
As such, it requires that you:
- Carefully audit your code and find all calls to the function and protect them with the lock.
- Ensure all calls to the function in the future are protected by the lock.
Lock Function Internally
The function may be locked directly within the function itself.
This requires changing the function to acquire the lock as the first action of the function, and releasing the lock as the final action of the function.
This can be achieved easily by using the context manager interface on the threading.Lock.
For example:
1 2 3 4 5 6 7 |
# custom function def work(): # declare scope of previously defined global lock variable global lock # acquire lock with lock: ... |
It requires that the same lock is always used within the function.
This can be achieved by:
- Defining the lock as a global variable.
- Passing the lock to the function each time it is called.
A benefit of this approach is that it does not require any change to the callers of the function, but does require that the lock be made available, potentially adding an argument to the function or an additional global variable.
Now that we know how to lock a function, let’s look at some worked examples.
Example of Locking a Function
We can explore how to lock a function from within the function itself.
The lock must be shared between each call to the function.
Two ways to achieve this is are:
- Pass the lock as an argument to the function.
- Define the lock as a global variable.
Let’s look at an example of each approach.
Lock Function With Argument
We can lock a function from within where the lock is provided by an argument.
First, we can define a function that takes the shared lock as an argument, acquires the lock then performs the task of the function.
The task() function below implements this.
1 2 3 4 5 6 |
# function to be called in a new thread def task(lock): # acquire the lock with lock: # block for a moment sleep(1) |
This function will be executed in a new thread.
In the main thread, we can first create the shared instance of the lock.
1 2 3 |
... # create shared lock lock = Lock() |
Next, we can create a new thread and configure it to execute our task() function and to pass the instance of the shared lock.
1 2 3 |
... # create a new thread thread = Thread(target=task, args=(lock,)) |
The new thread can then be started and the main thread can block until the new thread is finished.
1 2 3 4 5 |
... # start the new thread thread.start() # wait for the new thread to terminate thread.join() |
Tying this together, the complete example 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 locking a function from within the function from time import sleep from threading import Thread from threading import Lock # function to be called in a new thread def task(lock): # acquire the lock with lock: # block for a moment sleep(1) # create shared lock lock = Lock() # create a new thread thread = Thread(target=task, args=(lock,)) # start the new thread thread.start() # wait for the new thread to terminate thread.join() |
Running the example first creates the shared lock.
Next, the new thread is created and configured to run our task function called with the shared lock as an argument. The new thread is then started and the new thread blocks.
The thread executes the task() function first locking the function by acquiring the lock. It then executes the body of the function, releases the lock and returns from the function.
Next, let’s look at an example of locking a function using a lock as a global variable.
Lock Function With Global Variable
We can lock a function from within using a global lock.
This can be achieved by defining the lock as a global variable, then declaring the scope of the lock within the function before it is used, ensuring the correct scope is used.
Once the lock is available, the lock can be acquired within the function using the context manager interface, the body of the function executes, then the lock can be released automatically before the function returns.
The task() function below implements this.
1 2 3 4 5 6 7 8 |
# function to be called in a new thread def task(): # declare scope of global lock variable global lock # acquire the lock with lock: # block for a moment sleep(1) |
The lock can then be defined as a global variable.
1 2 3 |
... # create lock lock = Lock() |
A new thread can then be configured to execute our task() function.
1 2 3 |
... # create a new thread thread = Thread(target=task) |
Finally, the new thread can be started and the main thread can block until the new thread terminates.
1 2 3 4 5 |
... # start the new thread thread.start() # wait for the new thread to terminate thread.join() |
Tying this together, the complete example 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 |
# SuperFastPython.com # example of locking a function from within the function from time import sleep from threading import Thread from threading import Lock # function to be called in a new thread def task(): # declare scope of global lock variable global lock # acquire the lock with lock: # block for a moment sleep(1) # create lock lock = Lock() # create a new thread thread = Thread(target=task) # start the new thread thread.start() # wait for the new thread to terminate thread.join() |
Running the example first creates the shared lock.
Next, the new thread is created and configured to run our task function. The new thread is then started and the new thread blocks.
The thread executes the task() function first locking the function by acquiring the global lock. It then executes the body of the function, releases the lock and returns from the function.
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
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
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Takeaways
You now know how to lock a function in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Federico Vitale on Unsplash
Chuck Wegrzyn says
While you have covered most of the bases I would do it this way. This way requires very little code change, and nothing in the functions themselves or anything calling those functions.
Here’s a decorator that does the work (it’s very Pythonic…)
Then to lock a function you only need to do this:
The idea is to dynamically create the locks as needed, one for each function. The locks are named after the function, and stored in a dynamically created global __locked_functions.
Jason Brownlee says
Great suggestion, thanks for sharing.
Agreed, decorators are an excellent approach for more advanced practitioners!