Thinking in Behavior-Oriented Concurrency
This skill is a corrective. The default reflex when synchronizing concurrent
work is to reach for threads-and-locks primitives: shared state, a mutex,
a condition variable, a busy-wait loop, an atomic counter, an event flag, a
Future. In BOC, those answers are almost always wrong β not because they
break, but because they bypass the very mechanism that makes BOC safe and
fast.
The BOC question is not "what synchronization primitive do I need here?"
It is: "what cown is this work ordered against, and what behavior should run when that cown is free?"
Read this skill any time you catch yourself writing one of the smells below.
The smells
If you find yourself typing any of these inside, or in code that interacts with, a BOC program β stop and re-derive the design.
| Smell | What you almost certainly meant |
|---|---|
time.sleep(...) in a polling loop |
Schedule a behavior on the cown the predicate depends on. |
while not <flag>: ... busy-wait |
Same β make <flag> a cown and @when(flag) a behavior. |
threading.Event / Condition / Lock |
A cown plus a behavior chain. |
wait_for_<x>_version(target) polling |
@when(downstream_cowns) β let the cown graph order it. |
atomic_counter from Python |
A Cown(int) mutated inside @when(counter). |
Future, Queue.get(), "ferry one value out" |
return the value from a behavior; @when(that_behavior) reads it. |
time.sleep(0) "yield" |
@when() β the empty-cown behavior runs when a worker is free. |
if work_remaining: do_work(); else: stop in a thread loop |
A behavior loop: the behavior re-schedules itself with @when(state) on the same cown until done. |
The smells are signals that you are managing concurrency outside the runtime. The runtime cannot help you make that correct or fast.
The replacements
There are only a handful of BOC patterns. Almost every problem decomposes into one of them.
1. Sequencing on data β @when(cown)
A behavior runs when its cowns are free. That is the entire ordering
mechanism. If step2 must observe step1's effect on x, both behaviors
take x:
@when(x)
def step1(x):
x.value = "ready"
@when(x)
def step2(x):
assert x.value == "ready"You did not need a lock. You did not need an event. You did not need to
poll. The runtime acquired x for step1, released it, and only then gave
it to step2.
2. Fan-in / barrier β @when(cowns) vs @when(a, b, c)
There are two distinct shapes for "this behavior depends on multiple cowns" and choosing the right one matters.
Use @when(a, b, c) β separate positional arguments β when you know
at write-time exactly which cowns the behavior needs and they have
distinct roles. The decorated function takes one named parameter per
cown:
@when(account_a, account_b)
def transfer(src, dst): # two roles, two names
dst.value += src.value
src.value = 0Use @when(cowns) β a single list/tuple argument β when the set
is dynamic or homogeneous (its size is determined at runtime, or the
cowns play the same role). The decorated function takes one parameter
which is the list itself:
cowns = [Cown(i) for i in range(N)]
for c in cowns:
@when(c)
def producer(c):
... # writes whatever it writes
@when(cowns) # one list arg, not *cowns
def consumer(cowns):
total = sum(c.value for c in cowns) # cowns IS the listThis is the classical N-way barrier, expressed as data dependence: the
runtime acquires every cown in the list before the behavior runs, so the
consumer cannot start until every producer behavior has returned. Do
not spread the list with * β @when accepts the list directly, and
spreading would force you to know N at write-time, defeating the point.
Mixing the two forms β @when(anchor, cowns) β is also valid: the
behavior takes one named parameter (anchor) plus one list parameter.
3. Happens-after β chain on the prior behavior's result cown
@when returns a Cown holding the behavior's result. Pass that cown to a
later @when to enforce happens-after across unrelated data:
@when(x)
def writer(x):
x.value = compute()
@when(y, writer) # y is unrelated; writer is the result cown
def reader(y, w):
consume(y.value, w.value) # runs only after writer finishedHere reader touches its own data (y) and would otherwise be free to run
concurrently with writer. Depending on the writer result cown is what
serializes them: the runtime cannot acquire writer until that behavior has
returned, so reader sees its result via w.value.
4. Run when any worker is free β @when()
@when() with no arguments schedules a behavior with no data dependencies.
It runs as soon as a worker is available. Use this when you want some work
to happen in the background and you do not need to coordinate with any
particular cown β for example, sending a report after forks have been
released:
@when(left, right, hunger)
def take_bite(left, right, hunger):
left.value.use(); right.value.use()
hunger.value -= 1
if hunger.value == 0:
# forks released when this behavior returns; the report goes
# out from a fresh behavior so it does not delay the release.
@when()
def _():
send("report", ("full", index))@when() is also the BOC equivalent of "tail-call this on the worker
pool" β it lets the current behavior return promptly while the follow-up
work waits its turn.
5. Behavior loops β tail-recursive self-scheduling
To process work in chunks until done, do not write a while loop
inside one behavior β that pins one worker for the duration. Instead, the
behavior does one chunk and then schedules the next iteration on the
same cown:
def step(state: Cown[State]):
@when(state)
def _(state):
if state.value.done:
send("done", state.value.result)
return
state.value.do_one_chunk()
step(state) # tail-schedule next iterationThis is the BOC analogue of tail recursion. Each iteration releases the cown between chunks, so:
- other behaviors waiting on
statecan interleave between iterations, - the worker is returned to the pool between chunks, and
- work is naturally bounded by data availability β no busy-wait.
prime_factor.py (sieve_check β sieve_work β sieve_check) is the
canonical example in this repository.
6. Single-assignment rendezvous β the behavior's own result cown
@when returns a Cown holding whatever the behavior returns. That cown
is your rendezvous β there is no need to allocate a separate Cown(None)
and assign into it:
@when(x)
def compute(x):
return expensive(x.value) # the result lives in `compute`
@when(compute)
def consume(result): # result is a Cown
send("answer", result.value) # unwrap with .valueThis replaces Future / Queue for one-shot results. For streaming use
the message queue (send / receive) directly.
The BOC checklist
Before writing any synchronization, ask:
- What cown does this work depend on? If the answer is "none" you may
want
@when(). If the answer is "X" you want@when(X). If you know at write-time exactly which cowns you need, prefer the explicit form@when(X, Y, Z)β it is faster than the list form because the runtime can resolve each dependency by position rather than iterating a sequence. Only fall back to@when([X, Y, Z])(one list arg) when the set is dynamic or homogeneous. - Who reads my output? Their
@when(...)should include the cown I wrote to, or my behavior's result-cown. - Am I about to loop in one behavior? If the loop body has any release-friendly point, lift it into a behavior loop (pattern 5) so other work can interleave.
- Am I about to poll, sleep, or block? Find the cown the predicate depends on. Make the polling code a behavior on that cown.
- Am I about to use a
threading.*primitive inside a behavior? Almost certainly the wrong layer. Threads-and-locks primitives belong only at the BOC runtime boundary (test setup,wait(),receive()for assertions, the runtime's own internals).
When the classical answer is right
Classical synchronization is correct in three places:
- Outside the runtime, talking to it. The test thread blocking on
receive("assert")for assertion messages is a thread-level wait, and that is fine β it is the boundary between the test harness and the behavior graph. wait()itself. The library uses condvars internally to block the main thread until the runtime drains. Do not reinvent this.- C-level runtime internals.
_core.cuses mutexes and condvars because it implements BOC. User Python code should not.
If you are not in one of those three places and you are reaching for a classical primitive, walk back through the checklist.
Self-correction prompt
If you have already written code that uses time.sleep, wait_for_*, an
event flag, or a polling loop in a behavior or in code that schedules
behaviors, treat it as a defect. Ask:
Which cown carries the dependency I am polling on? Why is the consuming work not a behavior on that cown?
Rewrite to remove the classical primitive. The result is almost always shorter, faster, and provably free of races.