Python threading thread lock

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= ') # 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 ')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)

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= ')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)
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 ')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= ') 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 ')Code language: Python (python)
counter=10 counter=30 The final counter is 30Code language: Python (python)
  • 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=') 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 ') 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= ') 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 ') 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.

Источник

Python Threading Lock: Guide to Race-condition

Python Threading Lock

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

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.

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

intended outcome

  • 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()).

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' increments x by , 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 ')

Let’s understand the code above:

The output of race condition

  • 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.

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

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.

  • 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' increments x by , 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 ')

Let’s try to understand the code above:

Intended output on using threading lock

  • 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.

Locks vs RLocks

Intended output same for if context manager used.

FAQs on Python Threading Lock

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

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

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.

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

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.

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

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.

Источник

Читайте также:  Getting current year in java
Оцените статью