Welcome to another tutorial on Thread Synchronization in Lock Object in Python.
In multithreading when multiple threads are working simultaneously on a shared resource like a file (reading and writing data into a file), then to avoid concurrent modification error (multiple threads accessing the same resource leading to inconsistent data) some sort of locking mechanism is used where in when one thread is accessing a resource it takes a lock on that resource and until it releases that lock no other thread can access the same resource.
In the threading module of Python, a primitive lock is used for efficient multithreading. This Primitive lock assists us in the synchronization of two or more threads. The Lock class perhaps makes available the simplest synchronization primitive in Python programming language.
There are two states of Primitive lock, which are the locked or unlocked and are initially created in the unlocked state when the lock object is initialized. Also, it has two basic methods, the acquire() and release() methods.
Below is the basic syntax for creating a Lock object:
import threading
threading.Lock()
Also, the Lock objects use two methods, and they are:
This method is typically used to acquire the lock. When invoked without arguments, it blocks until the lock is unlocked.
The method can take two optional arguments:
This method is used to release an acquired lock. But, If the lock is locked, this method will reset it to unlocked, and return. In addition, this method can be called from any thread. So, when it is called, one out of the already waiting threads to acquire the lock is allowed to hold the lock.
The method also throws a RuntimeError if it is invoked on an unlocked lock.
Here we have a simple python program in the example, in which we have a class SharedCounter that will act as the shared resource between our threads.
Also, in the example below, there is a task method which we will call the increment() method. Since more than one thread will be accessing the same counter and incrementing its value, then there are chances of concurrent modification, that will possibly lead to inconsistent value for the counter.
import threading
import time
from random import randint
class SharedCounter(object):
def __init__(self, val = 0):
self.lock = threading.Lock()
self.counter = val
def increment(self):
print("Waiting for a lock")
self.lock.acquire()
try:
print('Acquired a lock, counter value: ', self.counter)
self.counter = self.counter + 1
finally:
print('Released a lock, counter value: ', self.counter)
self.lock.release()
def task(c):
# picking up a random number
r = randint(1,5)
# running increment for a random number of times
for i in range(r):
c.increment()
print('Done')
if __name__ == '__main__':
sCounter = SharedCounter()
t1 = threading.Thread(target=task, args=(sCounter,))
t1.start()
t2 = threading.Thread(target=task, args=(sCounter,))
t2.start()
print('Waiting for worker threads')
t1.join()
t2.join()
print('Counter:', sCounter.counter)
Output:
Waiting for a lockWaiting for a lockWaiting for worker threads
Acquired a lock, counter value: 0
Released a lock, counter value: 1
Waiting for a lock
Acquired a lock, counter value: 1
Released a lock, counter value: 2
Waiting for a lock
Acquired a lock, counter value: 2
Released a lock, counter value: 3
Done
Acquired a lock, counter value: 3
Released a lock, counter value: 4
Waiting for a lock
Acquired a lock, counter value: 4
If the thread uses the acquire() method to acquire a lock and then access a resource, suppose when accessing the resource some error occurs, what will be the outcome? In this case, no other thread will be able to access that resource, and thus we must access the resource inside the try block. Also, inside the finally block we can call the method release() to release the lock.