Deadlock in Python threading

Loading

Deadlock is a common problem in concurrent programming that occurs when two or more threads are waiting for each other to release a resource, creating a cycle where none of them can proceed. In Python, threading can lead to deadlocks when multiple threads hold locks and attempt to acquire additional locks without releasing the previous ones.

In this article, we will explore deadlocks in Python threading, their causes, and strategies to avoid them.


1. Understanding Python Threading

What is Threading?

Threading is a technique in Python that allows multiple threads (smallest units of execution) to run concurrently within a process. Python’s threading module provides an interface for managing threads.

Each thread runs in the same memory space but executes different tasks. However, managing shared resources among threads requires synchronization using locks, semaphores, or other mechanisms.

Locks in Python Threading

A lock is a synchronization primitive that ensures only one thread can access a shared resource at a time. In Python, locks are provided by the threading.Lock() class.

Example of Using Locks

import threading

lock = threading.Lock()

def critical_section():
lock.acquire()
try:
print("Thread is executing critical section")
finally:
lock.release()

thread1 = threading.Thread(target=critical_section)
thread2 = threading.Thread(target=critical_section)

thread1.start()
thread2.start()
thread1.join()
thread2.join()

Here, the lock ensures that only one thread executes the critical section at a time.


2. What is Deadlock?

Deadlock occurs when two or more threads are waiting indefinitely for each other to release resources. In Python, this usually happens when multiple locks are acquired in different orders, leading to a cyclic dependency.

Example of Deadlock

import threading
import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_task():
lock1.acquire()
print("Thread 1 acquired Lock 1")
time.sleep(1)

lock2.acquire()
print("Thread 1 acquired Lock 2")

lock2.release()
lock1.release()

def thread2_task():
lock2.acquire()
print("Thread 2 acquired Lock 2")
time.sleep(1)

lock1.acquire()
print("Thread 2 acquired Lock 1")

lock1.release()
lock2.release()

t1 = threading.Thread(target=thread1_task)
t2 = threading.Thread(target=thread2_task)

t1.start()
t2.start()

t1.join()
t2.join()

Step-by-Step Breakdown

  1. Thread 1 acquires Lock 1 and starts executing.
  2. Thread 2 acquires Lock 2 and starts executing.
  3. Thread 1 tries to acquire Lock 2 but it’s already held by Thread 2.
  4. Thread 2 tries to acquire Lock 1 but it’s already held by Thread 1.
  5. Both threads are now waiting for each other to release the lock, causing a deadlock.

Since neither thread can proceed, the program hangs indefinitely.


3. Causes of Deadlock

Deadlocks occur due to the following reasons:

  1. Circular Wait: A cycle exists where each thread is waiting for a resource held by another thread.
  2. Hold and Wait: A thread holds a lock while waiting for another lock.
  3. No Preemption: A thread cannot forcibly take a lock from another thread.
  4. Mutual Exclusion: Only one thread can hold a lock at a time.

4. Strategies to Prevent Deadlock

A. Avoid Nested Locks

Avoid acquiring multiple locks at once. If necessary, release locks as soon as possible.

Example

import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def safe_thread():
with lock1: # Acquire Lock 1
print("Acquired Lock 1")
with lock2: # Acquire Lock 2 inside Lock 1
print("Acquired Lock 2")

thread1 = threading.Thread(target=safe_thread)
thread2 = threading.Thread(target=safe_thread)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

Using with ensures proper lock handling without manually calling acquire() and release().


B. Use a Global Lock Ordering Strategy

Always acquire locks in a fixed order to prevent circular dependencies.

Example

import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_task():
first, second = (lock1, lock2) if threading.current_thread().name == "Thread-1" else (lock2, lock1)

with first:
print(f"{threading.current_thread().name} acquired {first}")
with second:
print(f"{threading.current_thread().name} acquired {second}")

t1 = threading.Thread(target=thread_task, name="Thread-1")
t2 = threading.Thread(target=thread_task, name="Thread-2")

t1.start()
t2.start()

t1.join()
t2.join()

This method ensures all threads acquire locks in the same order, preventing deadlocks.


C. Use try with Timeout for Locks

Instead of waiting indefinitely, set a timeout when acquiring locks.

Example

import threading

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread_task():
while True:
acquired1 = lock1.acquire(timeout=1)
acquired2 = lock2.acquire(timeout=1)

if acquired1 and acquired2:
print("Acquired both locks, performing task")
lock2.release()
lock1.release()
break
if acquired1:
lock1.release()
if acquired2:
lock2.release()

t1 = threading.Thread(target=thread_task)
t2 = threading.Thread(target=thread_task)

t1.start()
t2.start()

t1.join()
t2.join()

Here, timeout=1 ensures that if a thread cannot acquire the second lock within 1 second, it releases the first lock and retries.


D. Use threading.RLock()

RLock (Reentrant Lock) allows the same thread to acquire the lock multiple times without causing a deadlock.

Example

import threading

rlock = threading.RLock()

def thread_task():
with rlock:
print(f"{threading.current_thread().name} acquired RLock")
with rlock:
print(f"{threading.current_thread().name} re-acquired RLock")

t1 = threading.Thread(target=thread_task, name="Thread-1")
t2 = threading.Thread(target=thread_task, name="Thread-2")

t1.start()
t2.start()

t1.join()
t2.join()

Using RLock prevents deadlocks caused by the same thread trying to acquire a lock it already holds.

Leave a Reply

Your email address will not be published. Required fields are marked *