Topics

Lock Object

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.

 

Lock Object: Python Multithreading

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:

1. acquire(blocking=True, timeout=-1) method

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:

  1. The blocking flag if sent as False will not block the thread, if the lock is acquired by another thread already, it will return False as result. Also, when the value for this blocking flag is provided as True, then the calling thread will be blocked; but if another thread is holding the lock and once the lock is released then your thread will acquire the lock and return True.
  2. The timeout argument is used to provide a positive floating-point value, that typically specifies the number of seconds for which the calling thread will be blocked if another thread is holding the lock right now. It has a default value of -1 which means that the thread will be blocked for an indefinite time if it cannot acquire the lock immediately.

2. release() method

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.

 

Let’s take Examples

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

 

Note the following from the above example:

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.