05 · Handles and cancellation
Cancel the process you own, not every child in reach.
process handles allow targeted status, await, suspend, resume, kill, and random-state control. disable fork is broader: it kills all active child processes of the caller.
Targeted lifecycle
Capture → inspect → await or kill → verify
- →
- →
- →
- 01Captureprocess::self()
Child publishes its handle before the controller uses it.
- 02Inspectstatus()
FINISHED, RUNNING, WAITING, SUSPENDED, or KILLED guide legal action.
- 03Controlawait() / kill()
Wait naturally or terminate the exact process.
- 04Verifyterminal status
No live child or held resource remains.
Child publishes its handle before the controller uses it.
- Method
- await()
- The process API uses await, not a join method.
- Initialization
- Synchronize first
- A controller must not call a null handle before its child publishes it.
- disable label
- Terminates the named scope
- Code after a self-terminating named scope may never run.
A process handle turns implicit broad cancellation into a reviewed ownership operation with observable status.
Observe
Record when handles are assigned, which process owns them, every possible status, and all siblings that a broad cancellation could reach.
Decide
Use await for natural completion, kill for targeted cancellation, and a named wrapper process when a group must stop together.
Failure
A watchdog child's disable fork kills its own children, not its siblings; a parent's disable fork can kill unrelated background workers.
Prove
Assert handles are non-null before use, check status transitions, run winner and timeout paths, and verify no orphan remains after phase shutdown.
Publish handles before the controller races ahead
A forked child often assigns process::self() after it starts. The parent must wait for that publication or use a handshake before calling methods. With join_none, child startup itself is deferred until the parent yields.
process worker_p, timer_p;
event handles_ready;
fork
begin
worker_p = process::self();
-> handles_ready;
do_work();
end
begin
timer_p = process::self();
#100ns;
end
join_none
wait (worker_p != null && timer_p != null);
wait (worker_p.status() == process::FINISHED ||
timer_p.status() == process::FINISHED);
if (worker_p.status() != process::FINISHED) worker_p.kill();
if (timer_p.status() != process::FINISHED) timer_p.kill();
wait fork;disable fork follows caller ownership
When a process executes disable fork, it terminates all active descendants spawned directly by that process, with cascading effects. If a watchdog is merely a sibling of workers, its own disable fork does not reach those siblings. Put the race inside an owning wrapper or retain handles.
| Mechanism | Target | Primary risk |
|---|---|---|
| process.kill() | One known process | Handle publication and resource cleanup |
| disable fork | Caller's active child tree | Kills unrelated children |
| disable block_name | All activations of named scope in reach | Lexical breadth and self-termination |
| Cooperative stop flag | Code that checks it | Latency or blocked wait |
| Mailbox shutdown token | Channel consumer | One token per consumer or broadcast design |
Cleanup must release resources
Killing a process does not automatically return a semaphore permit, retract a mailbox message, or restore shared state. Structure critical sections so cancellation occurs outside them, or add a controller-owned cleanup protocol.
- Do not suspend a process while it owns a shared lock unless the design explicitly tolerates it.
- Use get_randstate/set_randstate only for intentional reproducibility; process random state is part of debug determinism.
- A disable of a named top scope terminates that scope, so a later display in the killed process does not execute.
- Avoid nested initial blocks; initial is a module/program/interface construct, not a procedural statement.
Explain it out loud
Interview reasoning checkpoints
It kills all active child processes of the caller, not only the branches from the nearest-looking fork. That may include earlier background children and their descendants.
- Identify the process executing disable fork.
- Enumerate every live immediate child it owns.
- Use a wrapper or handles if the intended target is narrower.
A strong answer describes runtime parentage and broad scope.
Assuming a watchdog sibling can use disable fork to kill its sibling workers.
First synchronize until the child has published a non-null handle, then call await() or inspect status. The process API method is await(), not join().
- Account for deferred child startup under join_none.
- Protect the handle publication with a wait, event plus state, or mailbox.
- After completion, verify resources and terminal status.
This tests both API knowledge and the initialization race.
Calling await on a handle before the forked child assigns it.
