SimPy Deadlock Explained: Why Your Simulation Hangs

Your simulation starts. Then stops. No error. No output. Just... nothing.

Welcome to deadlock.

What Is Deadlock?

Deadlock occurs when processes wait for each other in a circle. A waits for B. B waits for A. Nobody moves. Ever.

In SimPy, this typically happens with resources:

import simpy

def process_a(env, resource_1, resource_2):
    with resource_1.request() as req1:
        yield req1
        print(f"{env.now}: A has resource 1")
        yield env.timeout(1)

        with resource_2.request() as req2:
            yield req2  # STUCK - B has resource 2
            print(f"{env.now}: A has both resources")

def process_b(env, resource_1, resource_2):
    with resource_2.request() as req2:
        yield req2
        print(f"{env.now}: B has resource 2")
        yield env.timeout(1)

        with resource_1.request() as req1:
            yield req1  # STUCK - A has resource 1
            print(f"{env.now}: B has both resources")

env = simpy.Environment()
r1 = simpy.Resource(env, capacity=1)
r2 = simpy.Resource(env, capacity=1)

env.process(process_a(env, r1, r2))
env.process(process_b(env, r1, r2))
env.run()  # Hangs forever

The Four Conditions for Deadlock

Deadlock requires all four conditions:

  1. Mutual exclusion - Resources can't be shared
  2. Hold and wait - Hold one resource, wait for another
  3. No preemption - Can't forcibly take resources
  4. Circular wait - A waits for B, B waits for A

Break any one, and deadlock becomes impossible.

Solution 1: Consistent Ordering

Always acquire resources in the same order:

def process_a(env, resource_1, resource_2):
    # Always get resource 1 first, then resource 2
    with resource_1.request() as req1:
        yield req1
        with resource_2.request() as req2:
            yield req2
            yield env.timeout(5)

def process_b(env, resource_1, resource_2):
    # Same order: resource 1 first, then resource 2
    with resource_1.request() as req1:
        yield req1
        with resource_2.request() as req2:
            yield req2
            yield env.timeout(5)

Simple. Effective. No circular wait possible.

Solution 2: Timeout on Requests

Don't wait forever:

def process_with_timeout(env, resource_1, resource_2):
    with resource_1.request() as req1:
        yield req1

        req2 = resource_2.request()
        result = yield req2 | env.timeout(10)

        if req2 in result:
            # Got the resource
            yield env.timeout(5)
            resource_2.release(req2)
        else:
            # Timed out - release first resource and retry later
            print(f"{env.now}: Couldn't get resource 2, backing off")
            req2.cancel()
            yield env.timeout(1)  # Back off
            # Retry logic here

Solution 3: Try-All-Or-None

Request all resources at once:

def all_or_none(env, resources):
    """Request all resources or none."""
    requests = [r.request() for r in resources]

    # Wait for all
    results = yield simpy.AllOf(env, requests)

    try:
        yield env.timeout(5)  # Do work
    finally:
        for req, resource in zip(requests, resources):
            resource.release(req)

Solution 4: Resource Manager

Centralise resource allocation:

class ResourceManager:
    def __init__(self, env, resources):
        self.env = env
        self.resources = resources
        self.lock = simpy.Resource(env, capacity=1)

    def acquire_all(self, resource_names):
        """Acquire multiple resources atomically."""
        with self.lock.request() as lock_req:
            yield lock_req

            requests = []
            for name in resource_names:
                req = self.resources[name].request()
                yield req
                requests.append((name, req))

            return requests

    def release_all(self, requests):
        """Release multiple resources."""
        for name, req in requests:
            self.resources[name].release(req)

Detecting Deadlock

Add monitoring to catch hangs:

def deadlock_detector(env, processes, timeout=100):
    """Detect if simulation makes no progress."""
    last_time = env.now

    while True:
        yield env.timeout(timeout)

        if env.now == last_time:
            print(f"WARNING: No progress in {timeout} time units")
            print("Possible deadlock detected")

            # Log resource states
            for name, resource in resources.items():
                print(f"  {name}: {resource.count}/{resource.capacity} busy, "
                      f"{len(resource.queue)} waiting")

        last_time = env.now

Common Deadlock Patterns

The Dining Philosophers

Classic deadlock scenario:

def philosopher(env, left_fork, right_fork, name):
    while True:
        # Think
        yield env.timeout(random.expovariate(1))

        # Try to eat (potential deadlock!)
        with left_fork.request() as left:
            yield left
            # All philosophers grab left fork...

            with right_fork.request() as right:
                yield right  # ...then wait for right fork
                yield env.timeout(random.expovariate(1))

Fix: Always grab the lower-numbered fork first.

The Producer-Consumer

def producer(env, buffer, producer_lock):
    with producer_lock.request() as req:
        yield req
        # Wait for buffer space - but consumer needs producer_lock!
        yield buffer.put(item)

def consumer(env, buffer, producer_lock):
    with producer_lock.request() as req:
        yield req
        # Deadlock if buffer empty and producer waiting
        item = yield buffer.get()

Fix: Don't hold locks while waiting on buffers.

Debugging Deadlock

When your simulation hangs:

import signal
import sys

def timeout_handler(signum, frame):
    print("Simulation appears deadlocked!")
    print(f"Current time: {env.now}")

    # Print all pending events
    for event in env._queue[:10]:
        print(f"  Pending: {event}")

    sys.exit(1)

# Set timeout
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(60)  # 60 second timeout

env.run()
signal.alarm(0)  # Cancel alarm

Prevention Checklist

Before running: 1. ✓ All processes acquire resources in consistent order 2. ✓ No nested resource requests without timeout 3. ✓ Resources released promptly after use 4. ✓ No circular dependencies in process design

Summary

Deadlock is silent and deadly. Your simulation just stops.

Prevention: - Consistent ordering - Timeouts on requests - All-or-none acquisition - Centralised resource management

Deadlock-free code is careful code.

Next Steps


Strengthen Your Python Skills

If you're finding Python tricky, get up to speed quickly with the 10-Day Python Bootcamp. It's designed to give you the confidence and skills to write clean, effective code.

Start the Python Bootcamp