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 10
Code 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 sleep
Code language: Python (python)
Second, define a global variable called counter
whose value is zero:
counter = 0
Code 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 30
Code 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()
Code language: Python (python)
Summary
- A race condition occurs when two threads access a shared variable at the same time.
- Use a threading lock object to prevent the race condition
- Call the
acquire()
method of a lock object to acquire a lock. - Call the
release()
method of a lock object to release the previously acquired lock. - Use a threading lock object with the
with
statement to make it easier to acquire and release the lock.