How to use the Python Threading Lock to Prevent Race Conditions

Summary: in this tutorial, you’ll learn about race conditions and how to use the Python threading Lock object to prevent them.

What is a race condition

A race condition occurs when two or more threads try to access a shared variable simultaneously, leading to unpredictable outcomes.

In this scenario, the first thread reads the value from the shared variable. At the same time, the second thread also reads the value from the same shared variable.

Then both threads attempt to change the value of the shared variable. since the updates occur simultaneously, it creates a race to determine which thread’s modification is preserved.

The final value of the shared variable depends on which thread completes its update last. Whatever thread that changes the value last will win the race.

Race condition example

The following example illustrates a race condition:

from threading import Thread from time import sleep counter = 0 def increase(by): global counter local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') # create threads t1 = Thread(target=increase, args=(10,)) t2 = Thread(target=increase, args=(20,)) # start the threads t1.start() t2.start() # wait for the threads to complete t1.join() t2.join() print(f'The final counter is {counter}')Code language: Python (python)

In this program, both threads try to modify the value of the counter variable at the same time. The value of the counter variable depends on which thread completes last.

If the thread t1 completes before the thread t2, you’ll see the following output:

counter=10 counter=20 The counter is 20 Code language: Python (python)

Otherwise, you’ll see the following output:counter=20 counter=10 The final counter is 10Code language: Python (python)

How it works.

First, import Thread class from the threading module and the sleep() function from the time module:

from threading import Thread from time import sleepCode language: Python (python)

Second, define a global variable called counter whose value is zero:

counter = 0Code language: Python (python)

Third, define a function that increases the value of the counter variable by a number:

def increase(by): global counter local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}')Code language: Python (python)

Fourth, create two threads. The first thread increases the counter by 10 while the second thread increases the counter by 20:

t1 = Thread(target=increase, args=(10,)) t2 = Thread(target=increase, args=(20,))Code language: Python (python)

Fifth, start the threads:

t1.start() t2.start()Code language: Python (python)

Sixth, from the main thread, wait for the threads t1 and t2 to complete:

t1.join() t2.join()Code language: Python (python)

Finally, show the final value of the counter variable:

print(f'The final counter is {counter}')Code language: Python (python)

Using a threading lock to prevent the race condition

To prevent race conditions, you can use a threading lock.

A threading lock is a synchronization primitive that provides exclusive access to a shared resource in a multithreaded application. A thread lock is also known as a mutex which is short for mutual exclusion.

Typically, a threading lock has two states: locked and unlocked. When a thread acquires a lock, the lock enters the locked state. The thread can have exclusive access to the shared resource.

Other threads that attempt to acquire the lock while it is locked will be blocked and wait until the lock is released.

In Python, you can use the Lock class from the threading module to create a lock object:

First, create an instance the Lock class:

lock = Lock()Code language: Python (python)

By default, the lock is unlocked until a thread acquires it.

Second, acquire a lock by calling the acquire() method:

lock.acquire()Code language: Python (python)

Third, release the lock once the thread completes changing the shared variable:

lock.release()Code language: Python (python)

The following example shows how to use the Lock object to prevent the race condition in the previous program:

from threading import Thread, Lock from time import sleep counter = 0 def increase(by, lock): global counter lock.acquire() local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') lock.release() lock = Lock() # create threads t1 = Thread(target=increase, args=(10, lock)) t2 = Thread(target=increase, args=(20, lock)) # start the threads t1.start() t2.start() # wait for the threads to complete t1.join() t2.join() print(f'The final counter is {counter}')Code language: Python (python)

Output:

counter=10 counter=30 The final counter is 30Code language: Python (python)

How it works.

  • First, add a second parameter to the increase() function.
  • Second, create an instance of the Lock class.
  • Third, acquire a lock before accessing the counter variable and release it after updating the new value.

Using the threading lock with the with statement

It’s easier to use the lock object with the with statement to acquire and release the lock within a block of code:

import threading # Create a lock object lock = threading.Lock() # Perform some operations within a critical section with lock: # Lock was acquired within the with block # Perform operations on the shared resource # ... # the lock is released outside the with block Code language: PHP (php)

For example, you can use the with statement without the need of calling acquire() and release() methods in the above example as follows:

from threading import Thread, Lock from time import sleep counter = 0 def increase(by, lock): global counter with lock: local_counter = counter local_counter += by sleep(0.1) counter = local_counter print(f'counter={counter}') lock = Lock() # create threads t1 = Thread(target=increase, args=(10, lock)) t2 = Thread(target=increase, args=(20, lock)) # start the threads t1.start() t2.start() # wait for the threads to complete t1.join() t2.join() print(f'The final counter is {counter}') Code language: PHP (php)

Defining thread-safe Counter class that uses threading Lock object

The following illustrates how to define a Counter class that is thread-safe using the Lock object:

from threading import Thread, Lock from time import sleep class Counter: def __init__(self): self.value = 0 self.lock = Lock() def increase(self, by): with self.lock: current_value = self.value current_value += by sleep(0.1) self.value = current_value print(f'counter={self.value}') def main(): counter = Counter() # create threads t1 = Thread(target=counter.increase, args=(10, )) t2 = Thread(target=counter.increase, args=(20, )) # start the threads t1.start() t2.start() # wait for the threads to complete t1.join() t2.join() print(f'The final counter is {counter.value}') if __name__ == '__main__': main()

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *