About 3639 letters

About 18 minutes

#Python's thread synchronization

Threads within the same process share memory space and resources. When they operate on shared resources, it can lead to race conditions.

For example, in the code below, two threads each increment a shared counter 10 times. The expected result should be 20, but in reality, the output is random.

from threading import Thread import time # Counter class Counter: def __init__(self): self.__value = 0 # Increment the counter def increase(self): value = self.__value + 1 for _ in range(300000): # Extend execution time to increase the chance of error pass self.__value = value # Read the counter value def value(self): return self.__value # Global variable counter = Counter() # Entry function for the thread def worker(): time.sleep(1) global counter for _ in range(10): counter.increase() # Modify the global variable t1 = Thread(target=worker) # Create thread t2 = Thread(target=worker) t1.start() # Start thread t2.start() t1.join() # Wait for thread to finish t2.join() print(counter.value()) # Display the final result

Running it multiple times gives different results:

$ ./main.py 12 $ ./main.py 19 $ ./main.py 11 $ ./main.py 10 $ ./main.py 17

This happens due to a possible sequence like:

  • Thread 1 reads message as 3, then gets paused.
  • Thread 2 reads message as 3.
  • Thread 2 sets message to 3 + 1, then gets paused.
  • Thread 1 sets message to 3 + 1.

To ensure the program behaves as expected, we need thread synchronization when accessing shared resources.

#Mutex Lock

A mutex lock is a simple synchronization method. Once a thread acquires the lock, other threads trying to acquire it will be blocked until it is released.

In Python, you can use the Lock class from the threading module to create a mutex. Use acquire() to lock and release() to unlock.

from threading import Thread, Lock import time # Counter class Counter: def __init__(self): self.__value = 0 self.__lock = Lock() # Increment the counter def increase(self): self.__lock.acquire() # Acquire lock; other threads must wait value = self.__value + 1 for _ in range(300000): # Extend execution time to increase the chance of error pass self.__value = value self.__lock.release() # Release lock # Read the counter value def value(self): self.__lock.acquire() # Acquire lock return self.__value self.__lock.release() # Release lock (note: this line is unreachable) # Global variable counter = Counter() # Entry function for the thread def worker(): time.sleep(1) global counter for _ in range(10): counter.increase() # Modify the global variable t1 = Thread(target=worker) # Create thread t2 = Thread(target=worker) t1.start() # Start thread t2.start() t1.join() # Wait for thread to finish t2.join() print(counter.value()) # Display the final result

The Lock object supports the __enter__ and __exit__ methods for acquiring and releasing, so it can be used with the with statement. For example:

# Increment the counter def increase(self): with self.__lock: value = self.__value + 1 for _ in range(300000): pass self.__value = value

Created in 5/15/2025

Updated in 5/21/2025