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()
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.
Did you find this tutorial helpful ?