All articles

SimPy Deadlock Explained: Why Your Simulation Hangs

Published

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

Ready to build a model of your own?

The free SimPy Handbook covers the foundations; the Simulation Bootcamp covers everything else, from first principles to models a business will act on.