In https://bugs.python.org/issue36710 Victor wants to move away from the _PyRuntime C global, instead passing the _PyRuntimeState around explicitly. I'm in favor of the general idea, but not on a case-by-case basis like this. I left a comment on that issue (https://bugs.python.org/msg340945) that explains my position in more detail. In retrospect, I should have just posted here. :) So I've copied that comment below, as-is.
FYI, my intention is not to refuse Victor's objective in the issue. Rather, I want to make sure we have consensus on a valid broader objective on which to focus. This seemed like a perfect opportunity to start a discussion about it.
-eric
Status Quo
For simplicity sake, let's say nearly all the code operates relative to the 3 levels of runtime state:
- global - _PyRuntimeState
- interpreter - PyInterpreterState
- thread - PyThreadState
Furthermore, there are 3 groups of functions in the C-API:
- context-sensitive - operate relative to the current Python thread
- runtime-dependent - operate relative to some part of the runtime state, regardless of thread
- runtime-independent - have nothing to do with CPython's runtime state
Most of the C-API is context-sensitive. A small portion is runtime-dependent. A handful of functions are runtime-independent (effectively otherwise stateless helper functions that only happen to be part of the C-API).
Each context-sensitive function relies on there being a "runtime context" it can use relative to the current OS thread. That context consists of the current (i.e. active) PyThreadState, the corresponding PyInterpreterState, and the global _PyRuntimeState. That context is derived from data in TSS (see caveats below). This group includes most of the C-API.
Each runtime-dependent function operates against one or more runtime state target, regardless of the current thread context (or even if there isn't one). The target state (e.g. PyInterpreterState) is always passed explicitly. Again, this is only a small portion of the C-API.
Caveats: thread context
- for context-sensitive functions, we get the global runtime state from the global C variable (_PyRuntime) rather than via the implicit
- for some of the runtime-dependent functions that target _PyRuntimeState, we rely on the global C variable
All of this is the pattern we use currently. Using TSS to identify the implicit runtime context has certain benefits and costs:
benefits:
- sticking with the status quo means no backward incompatibility for existing C-extension code
- easier to distinguish the context-sensitive functions from the runtime-dependent ones
- (debatable) callers don't have to track, nor pass through, an extra argument
costs:
- extra complexity in keeping TSS correct
- makes the C-API bigger (extra macros, etc.)
Alternative
For every context-sensitive function we could add a new first parameter, "context", that provides the runtime context to use. That would be something like this:
struct { PyThreadState *tstate; ... } PyRuntimeContext;
The interpreter state and global runtime state would still be accessible via the same indirection we have now.
Taking this alternative would eliminate the previous costs. Having a consistent "PyRuntimeContext *context" first parameter would maintain the easy distinction from runtime-dependent functions. Asking callers to pass in the context explicitly is probably better regardless. As to backward compatibility, we could maintain a shim to bridge between the old way and the new.
About the C-global _PyRuntime
Currently the global runtime state (_PyRuntimeState) is stored in a static global C variable, _PyRuntime. I added it at the time I consolidated many of the existing C globals into a single struct. Having a C global makes it easy to do the wrong thing, so it may be good to do something else.
That would mean allocating a _PyRuntimeState on the heap early in startup and pass that around where needed. I expect that would not have any meaningful performance penalty. It would probably also simplify some of the code we currently use to manage _PyRuntime correctly.
As a bonus, this would be important if we decided that multiple-runtimes-per-process were a desirable thing. That's a neat idea, though I don't see a need currently. So on its own it's not really a justification for dropping a static _PyRuntime. :) However, I think the other reasons are enough.
Conclusions
This issue has a specific objective that I think is premature. We have an existing pattern and we should stick with that until we decide to change to a new pattern. That said, a few things should get corrected and we should investigate alternative patterns for the context-sensitive C-API.
As to getting rid of the _PyRuntime global variable in favor of putting it on the heap, I'm not opposed. However, doing so should probably be handled in a separate issue.
Here are my thoughts on actionable items:
- look for a better pattern for the context-sensitive C-API
- clearly document which of the 3 groups each C-API function belongs to
- add a "runtime" field to the PyInterpreterState pointing to the parent _PyRuntimeState
- (maybe) add a _PyRuntimeState_GET() macro, a la PyThreadState_GET()
- for context-sensitive C-API that uses the global runtime state, get it from the current PyInterpreterState
- for runtime-dependent C-API that targets the global runtime state, ensure the _PyRuntimeState is always an explicit parameter
- (maybe) drop _PyRuntime and create a _PyRuntimeState on the heap during startup to pass around