process-interaction.md
269 lines · skills/simpy/references/process-interaction.md at 5f2bc03
Most marks come from matching words in the file, so treat them as pointers.
Processes are event-yielding generators
A process function must return a generator object. env.process(generator) starts it through an urgent Initialize event. The generator executes until it yields an Event, then resumes with that Event's value or exception.
import simpy
def operation(env, duration):
yield env.timeout(duration)
return {"finished_at": env.now}
env = simpy.Environment()
process = env.process(operation(env, 3))
result = env.run(until=process)
assert result == {"finished_at": 3}Store a Process reference when another process must wait for or interrupt it. Process is itself an Event.
Waiting for another process
def stage(env, label, duration):
yield env.timeout(duration)
return label
def workflow(env):
first = env.process(stage(env, "first", 2))
first_result = yield first
second = env.process(stage(env, "second", 3))
second_result = yield second
return first_result, second_resultFor parallel activities, create all Processes before yielding:
def parallel(env):
a = env.process(stage(env, "a", 2))
b = env.process(stage(env, "b", 3))
results = yield a & b
assert results[a] == "a"
assert results[b] == "b"Condition results map Event objects to values. Keep the original Process/Event references.
Raw lines 27 to 56Shared one-shot events
A plain Event can passivate multiple waiters and broadcast one value:
def listener(env, signal, log, name):
value = yield signal
log.append((name, env.now, value))
env = simpy.Environment()
signal = env.event()
log = []
env.process(listener(env, signal, log, "a"))
env.process(listener(env, signal, log, "b"))
signal.succeed("go")
env.run()Events are one-shot. For repeated signals, retain the old Event, replace the shared attribute, then trigger the old Event; ensure all participants read that attribute:
class Clock:
def __init__(self, env):
self.env = env
self.tick = env.event()
def pulse(self):
current = self.tick
self.tick = self.env.event()
current.succeed(self.env.now)Passing separate local event variables to two loops and then "resetting" each local variable creates disconnected signals. Encapsulate ownership.
Raw lines 57 to 92Interrupt delivery
target_process.interrupt(cause=None) schedules an urgent Interruption. When processed, it:
- removes the target process's resume callback from its current target Event;
- throws
simpy.Interrupt(cause)into the generator immediately.
def worker(env, log):
try:
yield env.timeout(10)
log.append(("finished", env.now))
except simpy.Interrupt as interrupt:
log.append(("interrupted", env.now, interrupt.cause))
def controller(env, target):
yield env.timeout(3)
target.interrupt("maintenance")
env = simpy.Environment()
log = []
worker_process = env.process(worker(env, log))
env.process(controller(env, worker_process))
env.run()
assert log == [("interrupted", 3, "maintenance")]Interrupting a terminated process or the currently active process itself raises RuntimeError.
The target Event is not canceled
An interrupt removes the Process callback from the Event it was yielding; it does not cancel the Event. The generator can re-yield that same Event:
def temporarily_distracted(env, opening_event):
while True:
try:
return (yield opening_event)
except simpy.Interrupt:
yield env.timeout(1) # Handle interruption.
# Loop and wait for the original opening_event again.If the original Event occurred during handling, yielding it resumes immediately with its value.
Resource request events need an additional decision:
- still waiting: re-yield the same request;
- abandoning the wait: call
request.cancel(); - already acquired: release it when leaving.
Using the resource request as a context manager handles release/cancel on exception exit.
Raw lines 124 to 150Resumable work
A Timeout cannot be "paused." On interruption, calculate completed work and create a new Timeout for the remainder:
def resumable_job(env, total_work, log):
remaining = total_work
while remaining > 0:
started = env.now
try:
yield env.timeout(remaining)
remaining = 0
except simpy.Interrupt as interrupt:
remaining -= env.now - started
log.append(
{
"cause": interrupt.cause,
"remaining": remaining,
"time": env.now,
}
)Define whether interrupted setup is lost, retained, or repeated. The code above assumes linear preempt-resume work.
Raw lines 151 to 177PreemptiveResource interrupts
A PreemptiveResource generates the interrupt, not application code. The cause is a Preempted record:
def preemptible(env, resource, priority, work):
with resource.request(priority=priority, preempt=True) as request:
try:
yield request
yield env.timeout(work)
except simpy.Interrupt as interrupt:
cause = interrupt.cause
print(
"preempted by",
cause.by,
"used since",
cause.usage_since,
"resource",
cause.resource,
)Do not assume every Interrupt has those attributes; manually generated interrupt causes can be any object. Use isinstance(cause, simpy.resources.resource.Preempted) when the distinction matters.
Timeout/renege races
def impatient(env, resource, patience):
with resource.request() as request:
patience_event = env.timeout(patience)
result = yield request | patience_event
if request not in result:
return {"outcome": "reneged", "time": env.now}
yield env.timeout(2)
return {"outcome": "served", "time": env.now}At an exact tie, AnyOf may contain multiple events that occurred before the Condition was processed. Decide tie policy explicitly:
if request in result:
# This policy treats simultaneous grant/patience as served.
...The losing Timeout remains in the queue. The context manager cancels only a pending request, not arbitrary condition members.
Raw lines 205 to 229Failure propagation
An uncaught process exception fails the Process. A parent yielding that Process receives the exception:
def broken(env):
yield env.timeout(1)
raise ValueError("model invariant failed")
def supervisor(env):
try:
yield env.process(broken(env))
except ValueError:
return "handled"Do not broadly suppress failures merely to keep a simulation running. Convert only expected domain failures into explicit state; let invariant/programming errors fail tests.
Raw lines 230 to 250Deadlock and liveness checks
env.run() returning because the queue is empty does not prove that every intended process completed. Processes can remain waiting on untriggered events with no future trigger. Keep references to required completion Processes and run until an explicit completion Event; if the schedule empties first, SimPy raises RuntimeError.
For production models:
- set a numeric horizon and event/entity caps;
- count completed and unfinished entities;
- assert conservation of resources/items;
- detect zero-delay recurrence;
- trace a small deterministic scenario before stochastic experiments.
Sources
See sources.md for the versioned Process Interaction, Events, and resource guides and API references.