tl;dr We’re really close to having proper isolation between interpreters.
Hi All,
Over the last few years, a number of contributors[1] (including myself) have made a variety of changes in the code base to improve runtime/interpreter startup[2] and interpreter isolation. Unsurprisingly, my personal motivation has been primarily facilitating a per-interpreter GIL, but there are at least several good reasons for this class of improvement.
By “isolation” I’m talking about sharing any mutable data between interpreters. This especially applies to objects[3]. A small amount of shared mutable data is necessarily process-global, and interpreters will continue sharing that safely by using one or more locks (currently the GIL) to guard mutation. However, any other state (AKA mutable data) must be strictly tied to each interpreter. All this is covered in detail in PEP 684.
The biggest part, by far, of isolating interpreters has been eliminating C global variables (at least ones that aren’t const or effectively const). Since this part of the effort started in earnest ~5 years ago, we’ve gone from several thousand unsupported globals to, in the last few months, zero[4]. I genuinely appreciate the people that have made this happen!
One of the most complex parts of interpreter isolation has been (surprise, surprise) sorting out extension modules. It has required finding an effective and efficient way to isolate them to each interpreter. This work was started quite a few years ago by Martin von Löwis, but the bulk of the solutions have come from @encukou, who has put in a substantial amount of time and effort.
Relatedly, there has also been a lot of great work done in the last 3-4 years by quite a few people to port stdlib extension modules to multi-phase init (which isolates them). We still have a dozen or so modules left to be ported[5]. Help with wrapping up porting the remaining modules would be very appreciated! (I’d be glad to collaborate/instruct. Just ping me.)
Overall, we’re currently really close to reaching proper isolation between interpreters, to the point that a per-interpreter GIL is realistic for 3.12. Furthermore, in the process of all this we’ve ended up finding (and fixing) a bunch of bugs hidden in the runtime. We’ve exposed some useful capability through the C-API, and achieved (or facilitated) a number of performance improvements. We’ve advanced the effort to determine the future of the C-API, including by helping expose the value of explicit runtime contexts. Finally, it’s much easier to identify and reason about CPython’s runtime state now. IMHO, the runtime is in a better place as a result of this work.
Again, thanks to everyone that’s helped!
-eric
[1] It’s hard to identify everyone involved, but one key person was @vstinner. Victor had a hand in a lot of this work and it would have taken much, much longer without him.
[2] The bulk of the improvements to startup have been due, directly or indirectly, to the work @ncoghlan did around 2017(?), which were followed up extensively by @vstinner. Without those improvements, interpreter isolation would have been much harder to reason about and track, and thus much more difficult.
[3] Even otherwise effectively immutable objects (e.g. None) can’t be shared due to refcounts, at least not without object “immortality”. (See PEP 683.)
[4] This doesn’t count the objects that we will be making immortal or the handful of extension modules we still need to port to multi-phase init.
[5] I’m not counting the various test modules, which don’t impact real usage, nor builtin modules, which currently don’t have any unsupported globals (once we have immortal objects). In fact, I’d like to see all the remaining builtin modules ported to multi-phase init, since the machinery for single-phase init modules is a bit complex (making it harder to debug isolation issues in builtin modules), but that isn’t essential at the moment.