How to use Locks with Python Threads

In this tutorial we will discuss how to use Locks on Python Threads to protect our variables from synchronization problems.

But before we begin demonstrating how to create Locks in Python, let’s discuss the theory behind Threads and Thread Locks first.


Why do we need Thread Locks?

When we are dealing with just one thread, we don’t have to worry about synchronization as everything is happening sequentially. For example, we cannot begin reading from a resource while also writing to it.

However, when dealing with multiple threads we need to be alot more careful.

Let’s assume we have a variable that multiple threads want to access. As long as they only want to “read” from the variable, there is no need for Thread Locks. But if even one thread (or more) begins to modify this variable (a.k.a as shared resource) then we might run into a “race condition” and need a Thread Lock to maintain synchronization.

A Thread Lock “locks” a shared resource for the duration that a thread accesses it. This prevents other threads from accessing it in anyway until the thread “releases” the lock. Once it has been released, the (one of the) other threads may “acquire” the lock and access the resource.

This way we can prevent multiple threads from accessing the same resource at the same time.


Race Conditions

Without going into too much detail, Race Conditions basically involve threads over-riding each others operations. You also need to remember that even the simple act of incrementing a variable by “1” involves multiple operations.

(1) First you need to read the value of the variable, (2) then you need to increment that value by one. (3) Finally you update the variable with the new value. A total of three operations for a simple task.

A race condition can occur if the value of the variable is updated after a thread has read the value of the variable, but before it gets the chance to update the variable. In other words, the variable is updated by another thread while the first thread is busy in operation 2. This change however will be over-ridden because the first thread immediately over-writes it.

If you are interested in learning more about Race Conditions with actual code examples, refer to our dedicated tutorial on Race Conditions.


Creating Thread Locks in Python

Below is the code for creating a thread lock in Python. Let’s go through it step-by-step.

from threading import Thread, Lock
from time import sleep

lock = Lock()

def func(lock):
    print("Waiting for Lock")
    lock.acquire()

    print("Acquired Lock")
    # Execute code in Critical section
    sleep(1)

    lock.release()
    print("Released Lock")

thread = Thread(target=func, args=(lock,))
thread.start()
thread.join()

from threading import Thread, Lock
lock = Lock()

First we need to create the Thread Lock using the Lock Class from the Python threading module.

Here is our function that the thread will execute. First, we need to acquire() the lock. This is a “blocking” function, which will not let our execution continue unless the lock is “free”. In other words, if there is another thread accessing the resource, this thread must wait until it has released the lock.

def func(lock):
    lock.acquire()

    # Execute code in Critical section
    sleep(1)

    lock.release()

Once we have acquired the lock, we begin accessing the shared resource. This part of the code is called the “Critical Section”. Once we are done using it, we immediately release the lock using release().

It is important to release() the lock as soon as you are done using the shared resource, else the performance of your system will drop as other threads need to wait longer. Likewise, only acquire the lock when you need the shared resource. In simpler words, keep the size of the critical section small.

thread = Thread(target=func, args=(lock,))
thread.start()
thread.join()

Here we spawn a new thread and pass in “lock” as an argument. We can avoid passing it as an argument if we just make it a global variable.


Did you know? Just like locks for variables, there are also locks for Files in Python!


Thread Locks in Python – Example#2

Let’s take a look at a more practical example involving two threads. The code is the exact same as before, so let’s focus more on the output.

from threading import Thread, Lock
from time import sleep
import sys

print = lambda x: sys.stdout.write("%s\n" % x)
lock = Lock()

def func(lock, id):
    print(f"Thread {id}: Waiting for Lock")

    with lock:
        print(f"Thread {id}: Acquired the Lock")
        sleep(1)

    print(f"Thread {id}: Released Lock")


thread1 = Thread(target=func, args=(lock, 1))
thread2 = Thread(target=func, args=(lock, 2))

thread1.start()
thread2.start()

thread1.join()
thread2.join()
Thread 1: Waiting for Lock
Thread 1: Acquired the Lock
Thread 2: Waiting for Lock 
Thread 1: Released Lock
Thread 2: Acquired the Lock
Thread 2: Released Lock

The output shows us that “Thread 1” began waiting for the lock, and gained access to it immediately (because it was created first). On the other hand, when “Thread 2” began, it had to wait until “Thread 1” released the lock. Only then it was able to acquire it.

The last output line shows us that Thread 2 released the lock, after which the program ended.


Note:

print = lambda x: sys.stdout.write("%s\n" % x)

With this line we over-rode the standard print() function with a new one. This is a thread-safe way of printing to the console, otherwise the output often gets messed up with multiple threads printing to the console.

Try executing the code using the standard print() function to see it yourself.


This marks the end of the How to use Locks with Python Threads Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments