Introduction

Multithreading is a powerful concept in software development.  However, with this great power comes great responsibility. When multiple threads are accessing a shared resource simultaneously, it can lead to data inconsistency. To mitigate this problem, Python provides locks. In this tutorial, we'll focus on the threading.lock module in Python and how to use it to prevent multiple threads from accessing the same resource at the same time, thereby avoiding race conditions, deadlocks, and other concurrency issues. Let's dive right into it!

Table of Contents :

  • What is a Race Condition
  • How to use Lock to Prevent the Race Condition

What is a Race Condition :

  • A race condition is a situation in which 
    • the outcome of a program depends on the order and timing of events, and 
    • when two or more threads access shared resources simultaneously, it can result in unpredictable behavior.
  • Race conditions can cause programs to crash, hang or produce incorrect results
  • Code Sample : Race Condition Example:

import threading

# A shared variable
balance = 100

def withdraw(amount):
   global balance
   balance = balance amount

def deposit(amount):
   global balance
   balance = balance + amount

# Create two threads
t1 = threading.Thread(target=withdraw, args=(50,))
t2 = threading.Thread(target=deposit, args=(100,))

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

# Wait for the threads to finish
t1.join()
t2.join()

# Print the final balance
print("Final balance:", balance)


Explanation : 

  • In this example, we have two threads  t1  and  t2  that are accessing the shared variable  balance  simultaneously.
  •  t1  is withdrawing an amount of 50 from the account, while  t2  is depositing an amount of  100 .
  • Because the threads are executing concurrently, the final balance can be unpredictable and may not be what we expect.

How to use Lock to Prevent the Race Condition :

  • A threading  Lock  is a synchronization primitive that can be used to ensure that only one thread is accessing a shared resource at a time.
  • To use a  Lock 
    • we create a new instance and 
    • acquire it before accessing the shared resource, and 
    • release it once we're done.
  • Code Sample :

import threading

# A shared variable
balance = 100

# A Lock for the balance variable
lock = threading.Lock()

def withdraw(amount):
   global balance
   # Acquire the lock
   lock.acquire()
   balance = balance amount
   # Release the lock
   lock.release()

def deposit(amount):
   global balance
   # Acquire the lock
   lock.acquire()
   balance = balance + amount
   # Release the lock
   lock.release()

# Create two threads
t1 = threading.Thread(target=withdraw, args=(50,))
t2 = threading.Thread(target=deposit, args=(100,))

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

# Wait for the threads to finish
t1.join()
t2.join()

# Print the final balance
print("Final balance:", balance)


Explanation : 

  • Here, we have added a new  Lock  instance that we use to control access to the  balance  resource.
  • Before accessing the  balance  variable, 
    • we acquire the lock using  lock.acquire() , and 
    • once the operation is done we release the lock using  lock.release() .
  • This ensures that only one thread can access the  balance  variable at a time, thereby preventing the race condition.

Prev. Tutorial : Thread Pools

Next Tutorial : Multiprocessing in Python