#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
messageas 3, then gets paused. - Thread 2 reads
messageas 3. - Thread 2 sets
messageto 3 + 1, then gets paused. - Thread 1 sets
messageto 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