How to use monitors for safe multithreading in Python

In the world of concurrent programming, managing shared resources and ensuring data consistency is a complex and critical task. Python provides a built-in mechanism for handling these challenges through monitors, which use synchronization constructs to control access to shared resources and prevent race conditions.

In this Answer, we’ll explore monitors as a synchronization mechanism in Python.

Understanding monitors

A monitor is a high-level synchronization construct that encapsulates data and the procedures that operate on that data. It ensures that only one thread can access the monitor at a time, allowing for synchronized access to shared resources. Python provides monitors through the threading module, specifically with the Lock class.

Example

Let’s consider a scenario where multiple threads need to update a shared counter. Without synchronization, race conditions can occur, leading to unpredictable results. We can use a monitor to ensure that only one thread can modify the counter at a time.

In the example below, we define a CounterMonitor class, which encapsulates a counter and a lock. The increment method uses the lock to ensure that only one thread can modify the counter at any given time. We create multiple threads, and each thread increments the counter a specified number of times. When all threads have finished, we print the final value of the counter.

Multithreading
Multithreading

Code

Here is the code for the above example.

import threading
class CounterMonitor:
def __init__(self):
self.counter = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.counter += 1
def worker(counter, iterations):
for _ in range(iterations):
counter.increment()
if __name__ == "__main__":
counter = CounterMonitor()
num_threads = 4
iterations_per_thread = 1000
threads = []
for _ in range(num_threads):
thread = threading.Thread(target=worker, args=(counter, iterations_per_thread))
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("Counter Value:", counter.counter)

Explanation

Let’s take a closer look at the code above:

  • Line 1: We import the threading module, which provides functionality for working with threads in Python.

  • Lines 3–10: We define a custom class, CounterMonitor. This class will serve as a monitor to control access to a shared counter.

    • Line 4–6: We define the constructor method for the CounterMonitor class. It initializes an instance variable counter to 0. This variable represents the shared resource we want to protect with the monitor. It also creates a Lock object from the threading module and assigns it to an instance variable lock. The Lock will be used to synchronize access to the shared counter.

    • Line 8–10: We define a method, increment, within the CounterMonitor class. This method will be responsible for safely incrementing the shared counter while respecting the lock. Inside the function, we enter a critical section of code protected by the lock. It ensures that only one thread can execute the code within the with block at any given time, preventing race conditions. Inside the with block, we increment the shared counter by 1. It’s within the protected section, so only one thread can execute this line at a time.

  • Lines 12–14: We define a function, worker, that will be executed by multiple threads. The function takes two arguments: counter (an instance of the CounterMonitor class) and iterations (the number of times the counter should be incremented by the thread). Inside the function, we start a loop that will execute iterations times, where _ is used as a throwaway variable since it’s not used within the loop. Inside the loop, we call the increment method of the CounterMonitor instance counter to safely increment the shared counter.

  • Line 17: We create an instance of the CounterMonitor class, which initializes the shared counter and the associated lock.

  • Line 18: We set the number of threads that will be created to 4.

  • Line 19: We set the number of iterations each thread will perform to increment the counter. In this case, each thread will increment the counter 1000 times.

  • Line 21: We create an empty list to store the thread objects that will be created.

  • Lines 22–24: We start a loop that will run num_threads times. Inside the loop, a new threading.Thread object is created with the worker function as the target function. It also passes the counter instance and iterations_per_thread as arguments to the worker function. The newly created thread object is then appended to the threads list.

  • Lines 26–27: We start another loop that iterates through the list of thread objects. Inside the loop, each thread is started using the start() method. This initiates their execution.

  • Lines 29–30: We start another loop to wait for each thread to finish. Inside the loop, the join() method is called on each thread. This blocks the main program until all threads have completed.

  • Line 32: We print the final value of the shared counter after all threads have finished their work.

Conclusion

Monitors are a powerful tool in Python for managing concurrency and ensuring data consistency in multithreaded programs. By encapsulating shared resources and controlling access to them with locks, monitors help prevent race conditions and maintain the integrity of our data. When working with concurrent code in Python, understanding and utilizing monitors is essential for writing robust and thread-safe applications.

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved