Add C08, sending work to another interpreter - #177
Merged
Conversation
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.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_GetXIDatatries three routes in order rather than two.The third raises a
ValueErrorin another interpreter and takes apart what arrives here. It is anExecutionFailedcarrying a snapshot, soexcinfo.type.__name__is a string andisinstance(failed.excinfo, ValueError)is False. The cell ends by printingissubclass(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
readlineand_tkinterare refused by name with "does not support loading in subinterpreters". That isPy_mod_multiple_interpreters, which sits directly abovePy_mod_gilin 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
InterpreterPoolExecutorin 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 checkandjust versionsare green locally, 166 declared and 176 noted across 71 notebooks, and the browser probe has been rerun.Part of #27.