Skip to content

Add C08, sending work to another interpreter - #177

Merged
tamnd merged 1 commit into
mainfrom
c08-sending-work-to-another-interpreter
Sep 5, 2026
Merged

Add C08, sending work to another interpreter#177
tamnd merged 1 commit into
mainfrom
c08-sending-work-to-another-interpreter

Conversation

@tamnd

@tamnd tamnd commented Sep 5, 2026

Copy link
Copy Markdown
Owner

The eighth and last concurrency lesson, and the practical half of C04.

C04 built a second interpreter and found that the two of them share almost nothing. That was the good news and the bad news in one sentence: they cannot corrupt each other, and you now have a worker you cannot hand anything to. C08 is about the handing over. Which functions can cross, what happens to their arguments on the way, what comes back when the far side raises, and how many of these trips a second you can afford.

What is in it

Six cells, all of which skip themselves cleanly on a runtime that cannot make a second interpreter, which is what a browser tab does.

The first hands four kinds of callable to Interpreter.call. A function with no globals goes across and so does a lambda. A function that reads a module level name is refused, and so is a closure, both with a message about statelessness. That is the rule everybody trips over, because a recursive function written at the top level looks itself up as a global and therefore cannot cross.

The second checks seven values three ways at once: is_shareable, pickle.dumps, and an actual queue round trip. The lambda row is the interesting one. Both of the answers you expected say no and the value crosses anyway, which is how you can see with your own eyes that _PyObject_GetXIData tries three routes in order rather than two.

The third raises a ValueError in another interpreter and takes apart what arrives here. It is an ExecutionFailed carrying a snapshot, so excinfo.type.__name__ is a string and isinstance(failed.excinfo, ValueError) is False. The cell ends by printing issubclass(ci.ExecutionFailed, ValueError), which is also False, because that is the line that bites in real code.

The fourth imports six standard library modules into a subinterpreter. Four are fine, and readline and _tkinter are refused by name with "does not support loading in subinterpreters". That is Py_mod_multiple_interpreters, which sits directly above Py_mod_gil in the same header, so it is a clean callback to C07 with the opposite outcome: silence gets you a hard refusal here rather than a warning.

The fifth times twenty thousand queue round trips for three payloads. A small int and a thousand byte string manage well over a million a second. A hundred item list manages about a tenth of that. What you pay for is items, not bytes.

The sixth runs two jobs on a four worker InterpreterPoolExecutor in the same cell, and gets opposite answers. The arithmetic job is faster on four workers. The one that has to send a two hundred thousand item list is twelve times slower.

The recordings

Two Tier 1 recordings run the same two workloads three ways each, one after another, four threads and four interpreters, on a release build and a free threaded build from the same image pipeline.

On the build with the lock: arithmetic 507 ms one at a time, 494 ms on four threads, 208 ms on four interpreters. Threads do nothing, which is what the lock means, and interpreters give about two and a half times.

On the build without the lock: 506 ms, 179 ms on threads, 359 ms on interpreters. The ranking flips, because threads share their objects and interpreters have to copy theirs.

The list job is 6 ms one at a time and 255 ms across four interpreters on the first build, and 16 ms against 380 ms on the second. It is ruined on both, because the crossing is the job. Each recording also times the channel on its own so the ratio has a cause attached to it rather than just a number.

Also

Corrects C07's closing paragraph. It promised a C08 about what a second interpreter is, which is C04's material. It now promises the lesson that was actually written. From here the closing paragraph gets written after the next lesson's source reading rather than before it.

Three glossary terms: cross interpreter data, stateless function, interpreter pool. Six diagrams. Twelve citations. Both READMEs updated.

just check and just versions are green locally, 166 declared and 176 noted across 71 notebooks, and the browser probe has been rerun.

Part of #27.

The last concurrency lesson, and the practical half of C04. C04 made a
second interpreter and showed that the two share almost nothing. This one
asks what follows from that, which is how you give one of them a job and
what the handover costs.

Six cells. Four kinds of callable handed to Interpreter.call, where a
plain function and a lambda go across and a function reading a global and
a closure are both refused. Seven values checked three ways at once
against is_shareable, pickle and a real queue, which makes the middle
route visible as the row where both of the answers you expected say no and
the value crosses anyway. An exception raised in another interpreter,
caught here and taken apart to show it is a snapshot rather than the
exception. Six standard library modules imported into a subinterpreter,
where readline and _tkinter are refused by name. Twenty thousand queue
round trips timed for three payloads. And two jobs on a four worker
InterpreterPoolExecutor where one comes out faster and the other comes out
twelve times slower in the same cell.

Two Tier 1 recordings run the same two workloads three ways, one after
another, four threads and four interpreters, on a build with the lock and
a build without it. On the build with the lock the arithmetic goes from
507 ms to 494 ms with threads and to 208 ms with interpreters. On the
build without it threads take it to 179 ms and interpreters only to
359 ms. The list job is 6 ms one at a time and 255 ms across four
interpreters, and it stays that shape on both builds, because the
crossing is the job.

Three glossary terms: cross interpreter data, stateless function and
interpreter pool. Six diagrams. Twelve citations.

Also corrects C07's closing paragraph, which promised a C08 about what a
second interpreter is. That is C04's material, and this lesson is about
what it costs to use one.

Part of #27.
@tamnd tamnd added this to the M8 Concurrency and runtime milestone Sep 5, 2026
@tamnd tamnd added kind/lesson A chapter: prose, notebook, experiments, boss fight area/concurrency The GIL, free threading, subinterpreters and asyncio labels Sep 5, 2026
@tamnd
tamnd merged commit ac63678 into main Sep 5, 2026
16 checks passed
@tamnd
tamnd deleted the c08-sending-work-to-another-interpreter branch September 5, 2026 16:55
@tamnd tamnd mentioned this pull request Sep 5, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/concurrency The GIL, free threading, subinterpreters and asyncio kind/lesson A chapter: prose, notebook, experiments, boss fight

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant