All skills
microsoft avatar

/thinking-in-boc

@0884b96
by microsoftmicrosoft/bocpy197 stars
9

Think in Behavior-Oriented Concurrency, not threads-and-locks. Use when: writing or reviewing any bocpy code (library, examples, tests), about to reach for time.sleep / threading.Event / atomic counters / polling loops / wait_for_* helpers, designing how a downstream behavior observes an upstream one, scheduling work to run after the next worker is free, or building loop / tail-recursion patterns. Catches the reflex to apply classical synchronization to a problem that wants a cown.

  • 1 file
  • 10.1 KB
  • Updated 4 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/microsoft/bocpy/thinking-in-boc

This session only. Nothing lands on disk.

SKILL.md

β‰ˆ126 tokens always: the name and description. β‰ˆ2.4k when used: this file.

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 = 0

Use @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 list

This 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 finished

Here 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 iteration

This is the BOC analogue of tail recursion. Each iteration releases the cown between chunks, so:

  • other behaviors waiting on state can 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 .value

This replaces Future / Queue for one-shot results. For streaming use the message queue (send / receive) directly.

The BOC checklist

Before writing any synchronization, ask:

  1. 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.
  2. Who reads my output? Their @when(...) should include the cown I wrote to, or my behavior's result-cown.
  3. 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.
  4. Am I about to poll, sleep, or block? Find the cown the predicate depends on. Make the polling code a behavior on that cown.
  5. 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:

  1. 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.
  2. wait() itself. The library uses condvars internally to block the main thread until the runtime drains. Do not reinvent this.
  3. C-level runtime internals. _core.c uses 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.

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at 0884b96. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Activeupdated 4 months ago

README badge

README badge for microsoft/bocpy/thinking-in-boc