Doc future-subint payoffs for `_subint_forkserver`

Adds a "Future arch — what subints would buy us" section to
the module docstring, complementing the prior commit's
current-state rationale. Code is unchanged.

Frames the `subint` prefix as family-naming today (no actual
subinterp is created yet), then lays out the three concrete
wins that land once jcrist/msgspec#1026 unblocks PEP 684
isolated-mode subints:

- Cheaper forks — moving the parent's `trio.run()` into a
  subint shrinks the main-interp COW image the child inherits.
  The main interp becomes the literal forkserver: an
  intentionally-empty execution ctx whose only job is to call
  `os.fork()` cleanly.

- True parallelism — per-interp GIL means the forkserver
  thread on main and the trio thread on subint actually run in
  parallel. Spawn latency stops stalling the trio loop.

- Multi-actor-per-process — the architectural payoff. With
  per-interp-GIL subints, one process can host main + N
  subint-resident actor `trio.run()`s, and `os.fork()` reverts
  to the last-resort spawn (only when OS-level isolation is
  actually needed). Joins the story with the in-thread
  `_subint.py` backend: `subint` → in-process spawn,
  `subint_forkserver` → cross-process when a real OS boundary
  is required.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-04-27 18:20:10 -04:00
parent 3ab99d557a
commit 4b5176e2c3
1 changed files with 67 additions and 0 deletions

View File

@ -113,6 +113,73 @@ threads here are heavier than `trio.to_thread.run_sync`
calls see the "TODO" section further down for the audit
plan once those upstream pieces land.
Future arch what subints would buy us
---------------------------------------
The `subint` in this module's name is **family-naming
today** currently the implementation only uses a regular
worker thread on the main interp; no subinterpreter is
created anywhere in the parent or child. The naming becomes
*literal* once jcrist/msgspec#1026 unblocks isolated-mode
subints (PEP 684 per-interp GIL). Three concrete wins land
at that point:
**(1) Cheaper forks (smaller main-interp COW image)**
Today the parent's main interp carries the full tractor
stack: trio runtime, msgspec codecs, IPC layer, every
user module the actor imported. When the forkserver
worker calls `os.fork()` the child inherits ALL of that
as COW memory even though most gets overwritten when
the child boots its own `trio.run()`.
Move the parent's `trio.run()` into a subint (its own
`sys.modules` / `__main__` / globals) and the main
interp **stays minimal** just the forkserver-thread
plumbing + bare CPython. The main interp becomes the
*literal* forkserver: an intentionally-empty execution
context whose only job is to call `os.fork()` cleanly.
Inherited COW image shrinks proportionally.
**(2) True parallelism between forkserver and trio
(per-interp GIL)**
Today the forkserver worker and the trio.run() thread
share the main GIL when one runs the other waits.
Spawn requests briefly stall trio while the worker
takes the GIL to call `os.fork()`. PEP 684 isolated-
mode gives each subint its own GIL: forkserver thread
on main + trio on subint actually run in parallel.
Spawn latency drops, trio loop doesn't notice the
fork happening.
**(3) Multi-actor-per-process (the architectural prize)**
The bigger payoff and the reason `_subint.py` (the
in-thread `subint` backend) exists in parallel with
this module. With per-interp-GIL subints, one process
can host:
- main interp: forkserver thread + bookkeeping
- subint A: actor 1's `trio.run()`
- subint B: actor 2's `trio.run()`
- subint C: ...
`os.fork()` becomes the **last-resort** spawn used
only when a new OS process is actually required
(cgroups, namespaces, security boundary, multi-host
distribution). Within a single process, subint-per-
actor is radically cheaper: no fork, no COW, no
inherited-fd cleanup just `_interpreters.create()`
+ `_interpreters.exec()`.
The two backends converge on a coherent story:
`subint` in-process spawn (cheap, GIL-isolated),
`subint_forkserver` cross-process spawn (when you
truly need OS-level isolation). The forkserver isn't
the default mechanism; it's the bridge to a new
process when subint isolation isn't enough.
Implementation status what's wired today
-----------------------------------------