Python Threading Lock: Guide to Race-condition

In the following article, we will cover an important topic relating to OS, i.e., race condition, preventing it using Python’s threading module’s lock class.

Race condition: When two or more threads(here programs) try to access or use some shared resource at the same time(concurrently); to make changes to it, a condition occurs, i.e., race condition. Let’s elaborate:

Shared resource of programs 1 and 2
Shared resource of programs 1 and 2

Programs 1 & 2 both have read and write permissions to the resource. Let’s call programs 1 and 2, P1 and P2, for convenience and also assume P1 increments by 5 while P2 increments by 10.

Both P1 and P2 try to modify the shared resource.
Both P1 and P2 try to modify the shared resource.

Let’s say if P1 and P2 access x at the same time(concurrently) and try to modify it.

  • Say P1 increments x to 15 and store/replace it in x.
  • P2 modified x(which is 10 for P2) to 20 and then store/replace it in x.
  • Then we will endup with x = 20 as P2 will replace/overwrite P1’s incremented value.
  • This is the race condition, both P1 and P2 race to see who will write the value last.
  • Race condition can be avoided if locking is used(in python threading.lock()).
intended outcome
Intended outcome

Race condition example in Python

We will try to explain how a race condition can occur in a python code. Later in the solution part, we will cover the heart of the topic threading.Lock() to resolve race conditions.

import threading
import time

x = 10

def increment(increment_by):
    global x

    local_counter = x
    local_counter += increment_by

    time.sleep(1)

    x = local_counter
    print(f'{threading.current_thread().name} increments x by {increment_by}, x: {x}')

# creating threads
t1 = threading.Thread(target=increment, args=(5,))
t2 = threading.Thread(target=increment, args=(10,))

# starting the threads
t1.start()
t2.start()

# waiting for the threads to complete
t1.join()
t2.join()

print(f'The final value of x is {x}')

Let’s understand the code above:

  • We have imported the threading and time module of python in the first lines.
  • A variable x = 10 , is acting like a shared resource for threads t1 and t2.
  • Two threads t1 and t2 are created using the threading module, with target function pointing to increment.
  • t1 and t2 will try to modify the value of x in the increment function with 5 and 10 of increment as specified in args tuple.
  • start will intitiate the threads while join will wait for them to finish the execution as they will sleep for 1 sec in the increment function.
The output of race condition
The output of race condition

Solution using threading’s Lock

Race condition brings unpredictability to the shared variable/resource. This is due to the order in which threads run. If each thread is executed individually, the expected outcome can be achieved.

A mechanism that ensures only one program has access to the shared resource(here x) at a time, this region will be called the critical section. It is possible through locking.

Critical section using threading lock
Critical section

Locking is a synchronization(between two or more threads) mechanism. One process can lock the shared resource and make it inaccessible to others if operating on it.

Locking has two states:

  • Locked – means critical section is occupied, in binary i.e. 1
  • Unlocked – means critical section is vacant, in binary i.e 0.
import threading
from threading import Lock
import time

x = 10

def increment(increment_by,lock):
    global x

    lock.acquire()

    local_counter = x
    local_counter += increment_by

    time.sleep(1)

    x = local_counter
    print(f'{threading.current_thread().name} increments x by {increment_by}, x: {x}')

    lock.release()

lock = Lock()

# creating threads
t1 = threading.Thread(target=increment, args=(5,lock))
t2 = threading.Thread(target=increment, args=(10,lock))

# starting the threads
t1.start()
t2.start()

# waiting for the threads to complete
t1.join()
t2.join()

print(f'The final value of x is {x}')

Let’s try to understand the code above:

  • To avoid the race condition we have imported the Lock class of threading module and created an instance/object of it, named lock.
  • Lock has methods namely acquire and release, which as the name suggests acquires and releases the lock.
  • For instance, in increment function t1 aquires the lock and rights to operate on shared resource(here x). t2 can’t modify or interfare in the operation until the lock gets released for t1.
  • Firstly t1 completes its increment and finally t2 completes its increment on x , hence we obtain the intended value as the result.
Intended output on using threading lock
Intended output on using threading lock

Recommended Reading | Implementing Python Lock in Various Circumstances

Locks vs RLocks

LocksRLocks
Lock object can’t be acquired by another thread until and unless the thread using it releases it.RLock can be acquired any number of times by any thread.
Lock object isn’t owned by any thread.Whereas RLock can be owned by multiple threads.
Lock execution is faster.RLocks’s execution is comparatively slower.
Difference between locks and rlocks

Locks vs Semaphore

LocksSemaphore
The lock can’t be shared if one thread is has acquired it. Semaphore can have many processes of the same thread.
Operates on one buffer at a time.Can operate on multiple buffers at the same time.
Locks are objects of lock classSemaphores are integers with some values.
Locks have two methods acquire and release.While semaphore has wait and signal.
Difference between locks and semaphores

Threading lock using context manager

Context managers are a way of allocating and releasing some sort of resource exactly where you need it. For instance, opening and closing database connections. Let’s modify our above example:

import threading
from threading import Lock
import time

x = 10

lock = Lock()

def increment(increment_by,lock):
    global x

    with lock:
	    local_counter = x
	    local_counter += increment_by

	    time.sleep(1)

	    x = local_counter
	    print(f'{threading.current_thread().name} increments x by {increment_by}, x: {x}')

# creating threads
t1 = threading.Thread(target=increment, args=(5,lock))
t2 = threading.Thread(target=increment, args=(10,lock))

# starting the threads
t1.start()
t2.start()

# waiting for the threads to complete
t1.join()
t2.join()

print(f'The final value of x is {x}')

Using context manager, we acquire the lock, and it automatically releases the acquired lock.

Intended output same for if context manager used.
Intended output same for if context manager used.

FAQs on Python Threading Lock

How do you lock in multithreading in Python?

Firstly create a lock object of the threading module’s lock class, then put acquire and release methods inside the target function.

What is threading in Python?

Threading is running tasks concurrently(simultaneously). While in Python 3 implementations of threads, they merely appear to run simultaneously.

What is locking in threading?

Locking is a synchronization mechanism for threads. Once a thread acquires a lock, no other thread can access the shared resource until and unless it releases it. It helps to avoid the race condition.

What is a lock object in Python?

Lock object is an instance of lock class of threading module. It can acquire and release the shared resource in the target function.

What does the threading lock acquire method do?

The acquire method of threading lock acquire method acquires the lock. It allows the current thread to work in an isolated environment with the shared resource.

What is thread lock blocking?

A thread lock blocking occurs when one lock causes another thread to wait until the current thread is entirely done with the resources.

Threading’s lock vs. multiprocessing’s lock

The threading lock is faster and lighter than the multiprocessing lock as it doesn’t have to deal with shared semaphores. Moreover, when threads and processes are to be dealt with, then respective threading lock and multiprocessing lock should be used.

Conclusion

In this article, we looked at the working of lock class of threading module, the race condition, how it affects the functioning of threads, and most importantly, how it can be avoided using locking.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments