#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