How to Lock a Function in Python
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?
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.
...
# 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:
...
# 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:
...
# 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:
...
# 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:
# 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.
# 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.
...
# 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.
...
# 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.
...
# start the new thread
thread.start()
# wait for the new thread to terminate
thread.join()
Tying this together, the complete example is listed below.
# 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.
# 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.
...
# create lock
lock = Lock()
A new thread can then be configured to execute our task() function.
...
# 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.
...
# start the new thread
thread.start()
# wait for the new thread to terminate
thread.join()
Tying this together, the complete example is listed below.
# 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.
Takeaways
You now know how to lock a function in Python.
If you enjoyed this tutorial, you will love my book: Python Threading Jump-Start. It covers everything you need to master the topic with hands-on examples and clear explanations.