Skip to content

Commit 4b5176e

Browse files
committed
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
1 parent 3ab99d5 commit 4b5176e

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

tractor/spawn/_subint_forkserver.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,73 @@
113113
calls — see the "TODO" section further down for the audit
114114
plan once those upstream pieces land.
115115
116+
Future arch — what subints would buy us
117+
---------------------------------------
118+
119+
The `subint` in this module's name is **family-naming
120+
today** — currently the implementation only uses a regular
121+
worker thread on the main interp; no subinterpreter is
122+
created anywhere in the parent or child. The naming becomes
123+
*literal* once jcrist/msgspec#1026 unblocks isolated-mode
124+
subints (PEP 684 per-interp GIL). Three concrete wins land
125+
at that point:
126+
127+
**(1) Cheaper forks (smaller main-interp COW image)**
128+
129+
Today the parent's main interp carries the full tractor
130+
stack: trio runtime, msgspec codecs, IPC layer, every
131+
user module the actor imported. When the forkserver
132+
worker calls `os.fork()` the child inherits ALL of that
133+
as COW memory — even though most gets overwritten when
134+
the child boots its own `trio.run()`.
135+
136+
Move the parent's `trio.run()` into a subint (its own
137+
`sys.modules` / `__main__` / globals) and the main
138+
interp **stays minimal** — just the forkserver-thread
139+
plumbing + bare CPython. The main interp becomes the
140+
*literal* forkserver: an intentionally-empty execution
141+
context whose only job is to call `os.fork()` cleanly.
142+
Inherited COW image shrinks proportionally.
143+
144+
**(2) True parallelism between forkserver and trio
145+
(per-interp GIL)**
146+
147+
Today the forkserver worker and the trio.run() thread
148+
share the main GIL — when one runs the other waits.
149+
Spawn requests briefly stall trio while the worker
150+
takes the GIL to call `os.fork()`. PEP 684 isolated-
151+
mode gives each subint its own GIL: forkserver thread
152+
on main + trio on subint actually run in parallel.
153+
Spawn latency drops, trio loop doesn't notice the
154+
fork happening.
155+
156+
**(3) Multi-actor-per-process (the architectural prize)**
157+
158+
The bigger payoff and the reason `_subint.py` (the
159+
in-thread `subint` backend) exists in parallel with
160+
this module. With per-interp-GIL subints, one process
161+
can host:
162+
163+
- main interp: forkserver thread + bookkeeping
164+
- subint A: actor 1's `trio.run()`
165+
- subint B: actor 2's `trio.run()`
166+
- subint C: ...
167+
168+
`os.fork()` becomes the **last-resort** spawn — used
169+
only when a new OS process is actually required
170+
(cgroups, namespaces, security boundary, multi-host
171+
distribution). Within a single process, subint-per-
172+
actor is radically cheaper: no fork, no COW, no
173+
inherited-fd cleanup — just `_interpreters.create()`
174+
+ `_interpreters.exec()`.
175+
176+
The two backends converge on a coherent story:
177+
`subint` → in-process spawn (cheap, GIL-isolated),
178+
`subint_forkserver` → cross-process spawn (when you
179+
truly need OS-level isolation). The forkserver isn't
180+
the default mechanism; it's the bridge to a new
181+
process when subint isolation isn't enough.
182+
116183
Implementation status — what's wired today
117184
-----------------------------------------
118185

0 commit comments

Comments
 (0)