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.
Code
Here is the code for the above example.
import threadingclass CounterMonitor:def __init__(self):self.counter = 0self.lock = threading.Lock()def increment(self):with self.lock:self.counter += 1def worker(counter, iterations):for _ in range(iterations):counter.increment()if __name__ == "__main__":counter = CounterMonitor()num_threads = 4iterations_per_thread = 1000threads = []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
threadingmodule, 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 sharedcounter.Line 4–6: We define the constructor method for the
CounterMonitorclass. It initializes an instance variablecounterto0. This variable represents the shared resource we want to protect with the monitor. It also creates aLockobject from thethreadingmodule and assigns it to an instance variablelock. TheLockwill be used to synchronize access to the sharedcounter.Line 8–10: We define a method,
increment, within theCounterMonitorclass. This method will be responsible for safely incrementing the sharedcounterwhile respecting thelock. Inside the function, we enter a critical section of code protected by thelock. It ensures that only one thread can execute the code within thewithblock at any given time, preventing race conditions. Inside thewithblock, we increment the sharedcounterby1. 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 theCounterMonitorclass) anditerations(the number of times thecountershould be incremented by the thread). Inside the function, we start a loop that will executeiterationstimes, where_is used as a throwaway variable since it’s not used within the loop. Inside the loop, we call theincrementmethod of theCounterMonitorinstancecounterto safely increment the sharedcounter.Line 17: We create an instance of the
CounterMonitorclass, which initializes the sharedcounterand the associatedlock.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 thecounter1000times.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_threadstimes. Inside the loop, a newthreading.Threadobject is created with theworkerfunction as the target function. It also passes thecounterinstance anditerations_per_threadas arguments to theworkerfunction. The newly created thread object is then appended to thethreadslist.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
counterafter 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