Last Updated on September 12, 2022
You can lock an object using a mutex lock via the threading.Lock class.
In this tutorial you will discover how to lock an object in Python.
Let’s get started.
Need to Lock a Class
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 an object to protect it from being called concurrently from multiple threads.
How can we lock an object in Python?
Run loops using all CPUs, download your FREE book to learn how.
How to Lock an Object
We can lock an object 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:
Now that we know about mutex locks, let’s look at how to lock an object.
Lock an Object
An object can be locked using a mutex lock.
Recall that an object is an instance of a class. It has instance variables and methods.
We may wish that all methods on an object be serialized and executed in a mutually exclusive manner to avoid race conditions for the data managed within those methods.
This can be achieved by creating an instance of the lock in the constructor of the class.
We may have a custom class named CustomClass. We can then define a mutex lock as an instance variable and initialize it in the constructor.
For example:
1 2 3 4 5 6 |
# custom class class CustomClass: # constructor def __init__(self): # initialize mutex lock self.lock = threading.Lock() |
The lock can then be used within each method of the class.
We may acquire the lock as the first line of any method in the class, and release it as the last line of the method.
This can be achieved using the context manager.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 |
# custom class class CustomClass: # constructor def __init__(self): # initialize mutex lock self.lock = threading.Lock() # a method to do something def work(self): # acquire the lock with self.lock: # ... |
This shows how we might lock an instance of a class to protect it from race conditions.
Next, let’s look at a worked example.
Example of Locking an Object
We can develop an example of locking an object.
In this example we can define a new class that maintains a counter instance variable. We can create an instance of the class and call a method to increment the counter and another method to print the value of the counter. All access to the counter is protected by a mutex lock.
We can then increment the counter from many threads and the lock will protect changes to the counter from race conditions.
Incrementing a counter is a good functionality to protect from race conditions as it is inherently not thread safe. You can learn more about this in the tutorials:
First, we can define a class with the counter variable and our custom methods.
We will call the class ThreadSafeCounter.
1 2 3 |
# thread-safe counter class class ThreadSafeCounter: # ... |
The constructor will define and initialize the counter variable to the value zero, and a lock variable to protect the counter.
1 2 3 4 5 6 |
# constructor def __init__(self): # initialize mutex lock self.lock = Lock() # initialize a counter self.counter = 0 |
We can then define an increment() method that will first acquire the lock, and will then increment the counter value.
1 2 3 4 5 6 |
# increment the counter def increment(self): # acquire the lock with self.lock: # increment the counter self.counter += 1 |
Finally, we can define a report() function that acquires the lock, then prints the current value of the counter.
1 2 3 4 5 6 |
# report the counter value def report(self): # acquire the lock with self.lock: # report the counter value print(self.counter) |
Tying this together, the complete ThreadSafeCounter class 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 |
# thread-safe counter class class ThreadSafeCounter: # constructor def __init__(self): # initialize mutex lock self.lock = Lock() # initialize a counter self.counter = 0 # increment the counter def increment(self): # acquire the lock with self.lock: # increment the counter self.counter += 1 # report the counter value def report(self): # acquire the lock with self.lock: # report the counter value print(self.counter) |
Next, we can define a target task function to be executed in new threads.
This function will take an instance of the ThreadSafeCounter as an argument. It will then loop for a large number of times, e.g. 10,000, and call the increment() method each iteration.
The task() function below implements this.
1 2 3 4 |
# task function to increment the counter in a new thread def task(counter): for i in range(10000): counter.increment() |
In the main thread, we can first create an instance of our ThreadSafeCounter class to be shared among all threads.
1 2 3 |
... # create the counter counter = ThreadSafeCounter() |
Next, we can create a large number of threads that will all be configured to execute our task() function on the same ThreadSafeCounter class instance.
This can be achieved in a list comprehension where we will create a list of 1,000 configured threads.
1 2 3 |
... # create many threads threads = [Thread(target=task, args=(counter,)) for _ in range(1000)] |
We can then iterate over the list of threads and start them each in turn.
1 2 3 4 |
... # start threads for thread in threads: thread.start() |
The main thread can then block until all of the threads have finished.
1 2 3 4 |
... # wait for threads for thread in threads: thread.join() |
Finally, the value of the counter can be reported.
With 1,000 threads each incrementing the counter 10,000 times, we expect the final counter value to be 10,000,000 (ten million), given no race condition updating the counter.
1 2 3 |
... # report the counter value counter.report() |
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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# SuperFastPython.com # example of locking an object from threading import Thread from threading import Lock # thread-safe counter class class ThreadSafeCounter: # constructor def __init__(self): # initialize mutex lock self.lock = Lock() # initialize a counter self.counter = 0 # increment the counter def increment(self): # acquire the lock with self.lock: # increment the counter self.counter += 1 # report the counter value def report(self): # acquire the lock with self.lock: # report the counter value print(self.counter) # task function to increment the counter in a new thread def task(counter): for i in range(10000): counter.increment() # create the counter counter = ThreadSafeCounter() # create many threads threads = [Thread(target=task, args=(counter,)) for _ in range(1000)] # start threads for thread in threads: thread.start() # wait for threads for thread in threads: thread.join() # report the counter value counter.report() |
Running the example first creates an instance of our custom ThreadSafeCounter class.
Then 1,000 threads are created and configured to execute our task() function. The threads are then started and the main thread blocks until the threads finish.
Each thread loops for 10,000 iterations and each iteration it calls the increment() method on the shared instance of the ThreadSafeCounter class.
Each call to the increment() method first acquires the lock, then updates the value of the counter. If the lock has already been acquired, other threads will block and wait for the lock to become available. This ensures that all updates to the counter are mutually exclusive and serialized (one at a time).
All threads finish, then the final value of the counter is reported.
We can see that the expected value of ten million is reported, and will be reported every time the program is run due to the protection provided by locking the class instance.
1 |
10000000 |
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 an object in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Angie Rivera on Unsplash
Do you have any questions?