Home » Python Thread Deadlock Tutorial with Examples

Python Thread Deadlock Tutorial with Examples

1 Year Subscription
Java SE 11 Programmer I [1Z0-815] Practice Tests
Spring Framework Basics Video Course
Oracle Java Certification
Java SE 11 Programmer II [1Z0-816] Practice Tests
Java SE 11 Developer (Upgrade) [1Z0-817]

A deadlock is a situation where two or more threads are blocked forever, each waiting for the other to release a resource.

Deadlocks are common in concurrent programming when multiple threads try to acquire multiple locks but end up holding some locks and waiting indefinitely for others.

In this tutorial, we will cover:

  1. What is a Deadlock?
  2. How Deadlocks Occur in Python
  3. Basic Example of a Deadlock
  4. Avoiding Deadlocks
  5. Using threading.Lock() and threading.RLock()
  6. Deadlock Prevention Techniques
  7. Examples and Use Cases

Let’s dive into each topic with examples!

1. What is a Deadlock?

A deadlock occurs when two or more threads are waiting on resources held by each other, causing all of them to block indefinitely.

This situation arises in multi-threaded programs when multiple locks are involved, and threads acquire some locks but are waiting for other locks that are already held by another thread.

Characteristics of Deadlocks:

  • Mutual Exclusion: At least one resource must be held in a non-sharable mode.
  • Hold and Wait: A thread holds a resource and is waiting for additional resources held by other threads.
  • No Preemption: Resources cannot be forcibly taken from a thread.
  • Circular Wait: A set of threads is waiting in a circular chain for each other’s resources.

2. How Deadlocks Occur in Python

In Python, deadlocks typically happen when two or more threads attempt to acquire multiple locks in different orders.

If each thread holds one lock and tries to acquire another lock that is already held by another thread, the system will deadlock.

Example: Deadlock Scenario

Let’s consider a scenario where two threads each try to acquire two locks in a different order.

3. Basic Example of a Deadlock

Example 1: Demonstrating Deadlock

import threading
import time

# Create two locks
lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_task():
    print("Thread 1: Trying to acquire Lock 1")
    lock1.acquire()
    print("Thread 1: Acquired Lock 1")
    
    time.sleep(1)  # Simulate some work
    print("Thread 1: Trying to acquire Lock 2")
    lock2.acquire()  # This will cause a deadlock if Thread 2 holds Lock 2
    print("Thread 1: Acquired Lock 2")
    
    lock2.release()
    lock1.release()

def thread2_task():
    print("Thread 2: Trying to acquire Lock 2")
    lock2.acquire()
    print("Thread 2: Acquired Lock 2")
    
    time.sleep(1)  # Simulate some work
    print("Thread 2: Trying to acquire Lock 1")
    lock1.acquire()  # This will cause a deadlock if Thread 1 holds Lock 1
    print("Thread 2: Acquired Lock 1")
    
    lock1.release()
    lock2.release()

# Create threads
thread1 = threading.Thread(target=thread1_task)
thread2 = threading.Thread(target=thread2_task)

# Start threads
thread1.start()
thread2.start()

# Wait for threads to complete
thread1.join()
thread2.join()

print("Both threads have finished execution.")

Explanation:

  • Thread 1 acquires lock1 first and then attempts to acquire lock2.
  • Thread 2 acquires lock2 first and then attempts to acquire lock1.
  • Both threads end up holding one lock while waiting for the other, causing a deadlock.

Output:

Thread 1: Trying to acquire Lock 1
Thread 1: Acquired Lock 1
Thread 2: Trying to acquire Lock 2
Thread 2: Acquired Lock 2
Thread 1: Trying to acquire Lock 2
Thread 2: Trying to acquire Lock 1

At this point, both threads are stuck waiting indefinitely, creating a deadlock.


4. Avoiding Deadlocks

To avoid deadlocks, you need to ensure that:

  • Threads acquire locks in the same order.
  • You can use a timeout when trying to acquire locks, ensuring that a thread doesn’t block forever.

5. Using threading.Lock() and threading.RLock()

Using threading.RLock()

Python provides a re-entrant lock (RLock) that allows a thread to acquire the same lock multiple times. This helps prevent deadlocks in cases where recursive locking is needed.

Example 2: Using threading.RLock()

import threading
import time

# Create an RLock
rlock = threading.RLock()

def thread_task():
    with rlock:
        print(f"{threading.current_thread().name} acquired the lock")
        time.sleep(1)
        # Re-acquiring the same lock
        with rlock:
            print(f"{threading.current_thread().name} re-acquired the lock")
        print(f"{threading.current_thread().name} released the lock")

# Create and start multiple threads
thread1 = threading.Thread(target=thread_task, name="Thread-1")
thread2 = threading.Thread(target=thread_task, name="Thread-2")

thread1.start()
thread2.start()

# Wait for both threads to complete
thread1.join()
thread2.join()

print("All threads have completed.")

Explanation:

  • RLock() allows the same thread to acquire the lock multiple times without causing a deadlock.
  • This is useful when a thread needs to lock a resource multiple times within a recursive function or a nested lock scenario.

6. Deadlock Prevention Techniques

1. Acquire Locks in the Same Order

One of the simplest ways to avoid deadlock is to ensure that all threads acquire multiple locks in the same order. This prevents the circular wait condition that leads to deadlock.

Example 3: Avoiding Deadlock by Lock Ordering

import threading
import time

# Create two locks
lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_task():
    print("Thread 1: Trying to acquire Lock 1")
    with lock1:
        print("Thread 1: Acquired Lock 1")
        time.sleep(1)
        print("Thread 1: Trying to acquire Lock 2")
        with lock2:
            print("Thread 1: Acquired Lock 2")

def thread2_task():
    print("Thread 2: Trying to acquire Lock 1")
    with lock1:  # Same order as Thread 1
        print("Thread 2: Acquired Lock 1")
        time.sleep(1)
        print("Thread 2: Trying to acquire Lock 2")
        with lock2:
            print("Thread 2: Acquired Lock 2")

# Create and start threads
thread1 = threading.Thread(target=thread1_task)
thread2 = threading.Thread(target=thread2_task)

thread1.start()
thread2.start()

# Wait for threads to complete
thread1.join()
thread2.join()

print("Both threads have finished execution.")

Explanation:

  • Both thread1 and thread2 acquire the locks in the same order (lock1 first, then lock2), preventing a deadlock.

2. Using Timeout with acquire()

Another approach to avoid deadlock is to use the timeout parameter in acquire(). This ensures that if a thread cannot acquire a lock within a specified time, it will not wait indefinitely.

Example 4: Using Timeout in acquire()

import threading
import time

# Create two locks
lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1_task():
    print("Thread 1: Trying to acquire Lock 1")
    lock1.acquire()
    print("Thread 1: Acquired Lock 1")
    time.sleep(1)
    print("Thread 1: Trying to acquire Lock 2")
    if lock2.acquire(timeout=2):
        print("Thread 1: Acquired Lock 2")
        lock2.release()
    else:
        print("Thread 1: Could not acquire Lock 2, timed out.")
    lock1.release()

def thread2_task():
    print("Thread 2: Trying to acquire Lock 2")
    lock2.acquire()
    print("Thread 2: Acquired Lock 2")
    time.sleep(1)
    print("Thread 2: Trying to acquire Lock 1")
    if lock1.acquire(timeout=2):
        print("Thread 2: Acquired Lock 1")
        lock1.release()
    else:
        print("Thread 2: Could not acquire Lock 1, timed out.")
    lock2.release()

# Create and start threads
thread1 = threading.Thread(target=thread1_task)
thread2 = threading.Thread(target=thread2_task)

thread1.start()
thread2.start()

# Wait for threads to complete
thread1.join()
thread2.join()

print("Both threads have finished execution.")

Explanation:

  • Both threads use a timeout when acquiring the second lock (lock2 and lock1).
  • If they cannot acquire the lock within the timeout, they print a timeout message instead of blocking indefinitely.

7. Examples and Use Cases

Example 5: A

Real-World Deadlock Example (Bank Transfer Problem)

Consider a scenario where two bank accounts need to transfer money to each other. If the locks for both accounts are not acquired in the same order, it can lead to a deadlock.

import threading
import time

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

    def withdraw(self, amount):
        self.balance -= amount

    def deposit(self, amount):
        self.balance += amount

def transfer(from_account, to_account, amount):
    # Acquire locks in the same order to prevent deadlock
    with from_account.lock:
        print(f"Lock acquired on {from_account} by {threading.current_thread().name}")
        time.sleep(1)  # Simulate processing time
        with to_account.lock:
            print(f"Lock acquired on {to_account} by {threading.current_thread().name}")
            from_account.withdraw(amount)
            to_account.deposit(amount)
            print(f"Transfer of {amount} completed by {threading.current_thread().name}")

# Create two bank accounts
account1 = BankAccount(1000)
account2 = BankAccount(2000)

# Create threads for transfers
thread1 = threading.Thread(target=transfer, args=(account1, account2, 100), name="Thread-1")
thread2 = threading.Thread(target=transfer, args=(account2, account1, 200), name="Thread-2")

# Start the threads
thread1.start()
thread2.start()

# Wait for threads to complete
thread1.join()
thread2.join()

print(f"Account 1 balance: {account1.balance}")
print(f"Account 2 balance: {account2.balance}")

Explanation:

  • Each transfer function acquires locks on both accounts in the same order, preventing deadlock.
  • This example simulates a bank transfer between two accounts, ensuring the order of lock acquisition is consistent.

Summary of Key Concepts for Deadlock Prevention

Concept Description
Deadlock A situation where threads block each other by holding locks and waiting for each other’s resources.
Lock Ordering Ensuring that all threads acquire locks in the same order to prevent circular waiting.
Timeout with acquire() Using a timeout while acquiring locks to avoid waiting indefinitely and prevent deadlocks.
Reentrant Lock (RLock) A lock that can be acquired multiple times by the same thread, preventing deadlocks in recursive functions.

Conclusion

Deadlocks are a common problem in concurrent programming when multiple threads try to acquire multiple locks in different orders.

In Python, deadlocks can be avoided by using strategies like:

  • Acquiring locks in the same order.
  • Using timeouts with acquire() to prevent indefinite waiting.
  • Utilizing RLock() for recursive locking situations.

In this tutorial, we demonstrated:

  • How deadlocks occur using a basic example.
  • How to prevent deadlocks using proper lock ordering and timeouts.
  • The use of threading.Lock() and threading.RLock() for managing thread synchronization.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More