How to Lock Thread in Python

Muhammad Maisam Abbas Feb 02, 2024
  1. Race Condition in Python
  2. Thread Lock in Python
  3. Thread Lock Using with lock: in Python
How to Lock Thread in Python

This tutorial will discuss different methods to utilize a thread lock in Python.

Race Condition in Python

A race condition is a problem that occurs when multiple threads try to modify the same shared variable. All the threads read the same value from the shared variable at the same time. Then, all the threads try to modify the value of the shared variable. But, the variable only ends up storing the value of the last thread because it overwrites the value written by the previous thread. In this sense, there is a race between all the threads to see which one modifies the variable’s value in the end. This phenomenon is demonstrated with an example in the following code.

from threading import Thread

counter = 0


def increase(by):
    global counter
    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f"counter={counter}")


t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

Output:

counter=10
counter=20
The final counter is 20

We have a global shared variable counter = 0 and two threads t1 and t2. The thread t1 tries to increment the value of counter by 10 and the thread t2 tries to increment the value of counter by 20. In the above code, we run both threads simultaneously and try to modify the value of counter. By the above logic, the final value of counter should have the value 30. But, because of the race condition, the counter is either 10 or 20.

Thread Lock in Python

The thread lock is used to prevent the race condition. The thread lock locks access to a shared variable when used by one thread so that any other thread cannot access it and then removes the lock when the thread is not using the shared variable so that the variable is available to other threads for processing. The Lock class inside the threading module is used to create a thread lock in Python. The acquire() method is used to lock access to a shared variable, and the release() method is used to unlock the lock. The release() method throws a RuntimeError exception if used on an unlocked lock.

from threading import Thread, Lock

counter = 0


def increase(by, lock):
    global counter

    lock.acquire()

    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f"counter={counter}")

    lock.release()


lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

Output:

counter=10
counter=30
The final counter is 30

We created a globally shared variable counter=0 and two threads t1 and t2. Both threads target the same increase() function. The increase(by, lock) function takes two parameters. The first parameter is the amount by which it will increment counter, and the second parameter is an instance of the Lock class. In addition to the previous declarations, we also created an instance lock of the Lock class inside Python’s threading module. This lock parameter in the increase(by, lock) function locks access to the counter variable with the lock.acquire() function while it is modified by any thread and unlocks the lock with the lock.release() function when one thread has modified the counter variable. The thread t1 increments the value of counter by 10, and the thread t2 increments the value of counter by 20.

Due to the thread lock, the race condition does not occur, and the final value of counter is 30.

Thread Lock Using with lock: in Python

The problem with the previous method is that we have to carefully unlock each locked variable when one thread has completed processing. If not done correctly, our shared variable will only be accessed by the first thread, and no other thread will be granted access to the shared variable. This problem can be avoided by using context management. We can use with lock: and place all of our critical code inside this block. This is a much easier way to prevent race conditions. The following code snippet shows the use of with lock: to prevent the race condition in Python.

from threading import Thread, Lock

counter = 0


def increase(by, lock):
    global counter

    with lock:
        local_counter = counter
        local_counter += by
        counter = local_counter
    print(f"counter={counter}")


lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f"The final counter is {counter}")

Output:

counter=10
counter=30
The final counter is 30

We placed our code for incrementing counter inside with lock: block. The thread t1 increments the value of counter by 10, and the thread t2 increments the value of counter by 20. The race condition does not occur, and the final value of counter is 30. Furthermore, We do not need to worry about unlocking the thread lock.

Both methods do their job perfectly, i.e., both methods prevent the race condition from occurring, but the second method is far superior to the first one because it prevents us from the headache of dealing with the locking and unlocking of thread locks. It is also much cleaner to write and easier to read than the first method.

Muhammad Maisam Abbas avatar Muhammad Maisam Abbas avatar

Maisam is a highly skilled and motivated Data Scientist. He has over 4 years of experience with Python programming language. He loves solving complex problems and sharing his results on the internet.

LinkedIn

Related Article - Python Thread