PEP 703: Making the Global Interpreter Lock Optional (3.12 updates)

I finally found some time to read through PEP 703 in detail. It’s well written and covers a lot of ground effectively. Thanks for such diligent work, @colesbury! There are a small number of gaps, which I’ll note below. I didn’t notice any strictly incorrect information. I do have some questions. I’ll also provide a few related insights and recommendations. Thanks for all the great work!


Here are some initial observations/recommendations/corrections which would be worth addressing in the PEP:

  • GC: stop-the-world should only affect the current interpreter
  • the PEP should clearly enumerate every way it might break interpreter isolation, if any
  • PyThreadState: a status field already exists (AKA _status)
  • PyThreadState: we could track attached/detached status right now (AKA “holds_gil”)
  • some exclusions from the limited API seem problematic (e.g. critical sections)
  • consider adding Py_mod_gil_used for the Py_mod_gil slot, to let people be explicit
  • enabling the GIL (due to ext. module/$PYTHONGIL) should only affect the current interpreter
  • “Backward Compatibility”: ABI incompatibility is more than PyObject header (e.g. PyThreadState, PyTypeObject)
  • “Backward Compatibility”: clarify that “global state” includes per-interpreter module state
  • “Build Bots”: FYI, the buildbot fleet is already lagging behind demand, so ISTM doubling the demand will require a lot of new resources and supervision
  • “How to Teach This”: part of the purpose of “How to Teach This” is to identify the various things that would need to be taught, which would benefit readers of this PEP

Likewise, here are some questions:

  • how are tp_version_tag and keys_version kept thread-safe?
  • how to relax restrictions on specialization?
  • are single-phase init (legacy) extensions allowed? (presumably yes, and treated same as missing Py_mod_gil_not_used)
  • how to minimize burden on extension maintainers? (very similar to the concern with per-interpreter GIL)
  • are there other global no-gil details that should be per-interpreter?
  • does PyThread_Swap() work the same with no-gil? (i.e. same thread, different thread states, maybe different interpreters) (breaking this would be a serious problem)
  • just to restate, does the per-thread state (thread-local var?) accommodate swapping different PyThreadStates in and out (whether same interpreter or different interpreters)?
  • extensions: how to ensure thread-safety for global state in external libraries? (basically same as with per-interpreter GIL)
  • “Backward Compatibility”: how can we be crystal clear about which borrowed reference APIs are safe? (alt name + macro? hide old name?)
  • “Backward Compatibility”: does the PyMem_SetAllocator() restriction only apply to object allocator? (i.e. are “raw” & “mem” allocators left alone?)
  • “Backward Compatibility”: who might be affected by the restriction on PyMem_SetAllocator()?
  • “Backward Compatibility”: is it okay to replace the actual “malloc” (not via PyMem_SetAllocator)?
  • “Backward Compatibility”: how to enforce restrictions on when the object allocator should be used?
  • “Build Bots”: how to support double(?) the demand?
  • “Build Bots”: who is going to set up and admin the new buildbots?
  • “Rejected Ideas”: how would introducing write barriers break the C-API? (this is relevant to recent C-API discussions)
  • what would it take to build a python binary with both modes provided?
  • could no-gil be done as a single ABI?
  • “Integration”: merge base mimalloc sooner, regardless of no-gil?
  • “Integration”: what other parts might we want to merge, regardless of no-gil?
  • “Integration”: in what other ways could the no-gil patch be split up? (seems like a lot of it is pretty deeply coupled)

Here’s something we should do in 3.12+ from which no-gil can benefit:

Let’s rename the “own_gil” field of PyInterpreterConfig to just “gil”. It’s currently a bool, so we’d have two values: “OWN” and “SHARED”. With no-gil we’d add two more values: “NEVER” (strictly disallow unsupported extensions, AKA PYTHONGIL=0) and “NO”. What would PYTHONGIL=1 match?

(This should be done in 3.12 since PyInterpreterConfig is new, so we wouldn’t have to break compatibility or deprecate it. I’ll make the change right away if our dear 3.12 release manager, @thomas, is okay with it. See gh-105603.)


Here’s one idea on how to build a single python binary with both modes provided (hence no ABI incompatibility for extensions and no new ABI tag):

  • use code generation to produce a distinct version of each affected C-API/ABI item
  • add macros matching the existing C-API for the relevant symbols/typedefs
  • use the build-time flag to dictate which version of the functions, etc. the corresponding macros point to

(There may be good reasons not to do this, or it might be viable. I didn’t spend much time thinking it through, but at first glance it seemed in the direction of reasonable.)


Here are some questions related to enabling the GIL at runtime:

  • what extra work (overhead) is skipped after the GIL is enabled? what extra work sticks around?
  • how to enable GIL without keeping any of the overhead?
  • can the GIL be re-disabled after it is enabled (e.g. by single-phase init extension)?
  • “Open Issues”: how would a runtime-controlled no-gil be different from the proposed PYTHONGIL/Py_mod_gil effect?

Related, how hard would it be to make no-gil selectable at runtime for each interpreter separately? That would be especially useful if the mode with the GIL were to use the code that doesn’t have any of the no-gil overhead (e.g. via generated code like conjectured above). A per-interpreter no-gil would allow folks to mix and match more safely, I think. It could facilitate faster adoption and less pressure on extension maintainers.


Finally, here’s one last thing to consider (one which echoes what I’ve heard others say on this thread and elsewhere):

The PEP is a bit timid about the feature’s end game. I’m particularly referring to the “Python Build Modes” Open Issue. That position’s understandable given past feedback from core developers. However, given the potential costs for core development and for the community, an explicit commitment in the PEP to the final goal is important.

Let’s be honest. Adding the --disable-gil build flag doesn’t make sense if the intention isn’t to eventually make no-gil the default/only runtime option. Why should the community invest [valuable, mostly volunteer] time to supporting no-gil if there’s even a remote chance it gets yanked? It would be “too big to fail”. Furthermore, I expect the maintenance burden of the two build/runtime modes will be enough that the core team will not want to support both forever.

To me, it’s clear that no-gil would never get yanked once we start down the path, so the PEP should be explicit about that expected outcome. Otherwise I think you’re being a bit disingenuous.

Along those lines, it seems like a runtime mode would be inevitable. The current proposal is for a build-time flag, with a runtime-switching fallback. I recognize that this approach is at least partly at the recommendation of the core devs. However, if we’re not going back once we have it, and the runtime mode is inevitable, then why go through all the pain that the build mode approach introduces? If the PEP doesn’t change on this then it should explain why we should take on the burden of a second build mode when we are planning to converge on just one eventually.

17 Likes