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

Updated: 5/21/2025

Author: PlanC

Created: 5/15/2025