Skip to content

Commit d08b88d

Browse files
committed
gh-99633: New generator/coroutine _context property
Add a new `_context` property to generator (and coroutine) objects to get/set the "current context" that is observed by (and only by) the generator and the functions it calls. When `generator._context` is set to `None` (the default), the generator is called a "dependent generator". It behaves the same as it always has: the "current context" observed by the generator is the thread's context. This means that the observed context can change arbitrarily during a `yield`; the generator *depends* on the sender to enter the appropriate context before it calls `generator.send`. When `generator._context` is set to a `contextvars.Context` object, the generator is called an "independent generator". It acts more like a separate thread with its own independent context stack. The value of `_context` is the head of that independent stack. Whenever the generator starts or resumes execution (via `generator.send`), the current context temporarily becomes the generator's associated context. When the generator yields, returns, or propagates an exception, the current context reverts back to what it was before. The generator's context is *independent* from the sender's context. If an independent generator calls `contextvars.Context.run`, then the value of the `_context` property will (temporarily) change to the newly entered context. If an independent generator sends a value to a second independent generator, the second generator's context will shadow the first generator's context until the second generator returns or yields. The `generator._context` property is private for now until experience and feedback is collected. Nothing is using this yet, but that will change in future commits. Motivations for this change: * First, this change makes it possible for a future commit to add context manager support to `contextvars.Context`. A `yield` after entering a context causes execution to leave the generator with a different context at the top of the context stack than when execution started. Swapping contexts in and out when execution suspends and resumes can only be done by the generator itself. * Second, this paves the way for a public API that will enable developers to guarantee that the context remains consistent throughout a generator's execution. Right now the context can change arbitrarily during a `yield`, which can lead to subtle bugs that are difficult to root cause. (Coroutines run by an asyncio event loop do not suffer from this same problem because asyncio manually sets the context each time it executes a step of an asynchronous function. See the call to `contextvars.Context.run` in `asyncio.Handle._run`.) * Finally, this makes it possible to move the responsibility for activating an async coroutine's context from the event loop to the coroutine, where it more naturally belongs (alongside the rest of the execution state such as local variable bindings and the instruction pointer). This ensures consistent behavior between different event loop implementations. Example: ```python import contextvars cvar = contextvars.ContextVar('cvar', default='initial') def make_generator(): yield cvar.get() yield cvar.get() yield cvar.get() yield cvar.get() cvar.set('updated by generator') yield cvar.get() gen = make_generator() print('1.', next(gen)) def callback(): cvar.set('updated by callback') print('2.', next(gen)) contextvars.copy_context().run(callback) print('3.', next(gen)) cvar.set('updated at top level') print('4.', next(gen)) print('5.', next(gen)) print('6.', cvar.get()) ``` The above prints: ``` 1. initial 2. updated by callback 3. initial 4. updated at top level 5. updated by generator 6. updated by generator ``` Now add the following line after the creation of the generator: ```python gen._context = contextvars.copy_context() ``` With that change, the script now outputs: ``` 1. initial 2. initial 3. initial 4. initial 5. updated by generator 6. updated by top level ```
1 parent f6ae9d7 commit d08b88d

File tree

14 files changed

+684
-39
lines changed

14 files changed

+684
-39
lines changed

Include/cpython/pystate.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ struct _ts {
164164
PyObject *async_gen_firstiter;
165165
PyObject *async_gen_finalizer;
166166

167-
PyObject *context;
168167
uint64_t context_ver;
169168

170169
/* Unique thread state id. */

Include/internal/pycore_context.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
# error "this header requires Py_BUILD_CORE define"
66
#endif
77

8+
#include "cpython/context.h"
9+
#include "cpython/genobject.h" // PyGenObject
810
#include "pycore_hamt.h" // PyHamtObject
911

1012
#define CONTEXT_MAX_WATCHERS 8
@@ -30,6 +32,46 @@ struct _pycontextobject {
3032
int ctx_entered;
3133
};
3234

35+
// Resets a coroutine's independent context stack to ctx. If ctx is NULL or
36+
// Py_None, the coroutine will be a dependent coroutine (its context stack will
37+
// be empty) upon successful return. Otherwise, the coroutine will be an
38+
// independent coroutine upon successful return, with ctx as the sole item on
39+
// its context stack.
40+
//
41+
// The coroutine's existing stack must be empty (NULL) or contain only a single
42+
// entry (from a previous call to this function). If the coroutine is
43+
// currently executing, this function must be called from the coroutine's
44+
// thread.
45+
//
46+
// Unless ctx already equals the coroutine's existing context stack, the
47+
// context on the existing stack (if one exists) is immediately exited and ctx
48+
// (if non-NULL) is immediately entered.
49+
int _PyGen_ResetContext(PyThreadState *ts, PyGenObject *self, PyObject *ctx);
50+
51+
// Makes the given coroutine's context stack the active context stack for the
52+
// thread, shadowing (temporarily deactivating) the thread's previously active
53+
// context stack. The context stack remains active until deactivated with a
54+
// call to _PyGen_DeactivateContext, as long as it is not shadowed by another
55+
// activated context stack.
56+
//
57+
// Each activated context stack must eventually be deactivated by calling
58+
// _PyGen_DeactivateContext. The same context stack cannot be activated again
59+
// until deactivated.
60+
//
61+
// If the coroutine's context stack is empty this function has no effect.
62+
void _PyGen_ActivateContext(PyThreadState *ts, PyGenObject *self);
63+
64+
// Deactivates the given coroutine's context stack, un-shadowing (reactivating)
65+
// the thread's previously active context stack. Does not affect any contexts
66+
// in the coroutine's context stack (they remain entered).
67+
//
68+
// Must not be called if a different context stack is currently shadowing the
69+
// coroutine's context stack.
70+
//
71+
// If the coroutine's context stack is not the active context stack this
72+
// function has no effect.
73+
void _PyGen_DeactivateContext(PyThreadState *ts, PyGenObject *self);
74+
3375

3476
struct _pycontextvarobject {
3577
PyObject_HEAD
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#ifndef Py_INTERNAL_CONTEXTCHAIN_H
2+
#define Py_INTERNAL_CONTEXTCHAIN_H
3+
4+
#ifndef Py_BUILD_CORE
5+
# error "this header requires Py_BUILD_CORE define"
6+
#endif
7+
8+
#include "pytypedefs.h" // PyObject
9+
10+
11+
// Circularly linked chain of multiple independent context stacks, used to give
12+
// coroutines (including generators) their own (optional) independent context
13+
// stacks.
14+
//
15+
// Detailed notes on how this chain is used:
16+
// * The chain is circular simply to save a pointer's worth of memory in
17+
// _PyThreadStateImpl. It is actually used as an ordinary linear linked
18+
// list. It is called "chain" instead of "stack" or "list" to evoke "call
19+
// chain", which it is related to, and to avoid confusion with "context
20+
// stack".
21+
// * There is one chain per thread, and _PyThreadStateImpl::_ctx_chain::prev
22+
// points to the head of the thread's chain.
23+
// * A thread's chain is never empty.
24+
// * _PyThreadStateImpl::_ctx_chain is always the tail entry of the thread's
25+
// chain.
26+
// * _PyThreadStateImpl::_ctx_chain is usually the only link in the thread's
27+
// chain, so _PyThreadStateImpl::_ctx_chain::prev usually points to the
28+
// _PyThreadStateImpl::_ctx_chain itself.
29+
// * The "active context stack" is always at the head link in a thread's
30+
// context chain. Contexts are entered by pushing onto the active context
31+
// stack and exited by popping off of the active context stack.
32+
// * The "current context" is the top context in the active context stack.
33+
// Context variable accesses (reads/writes) use the current context.
34+
// * A *dependent* coroutine or generator is a coroutine or generator that
35+
// does not have its own independent context stack. When a dependent
36+
// coroutine starts or resumes execution, the current context -- as
37+
// observed by the coroutine -- is the same context that was current just
38+
// before the coroutine's `send` method was called. This means that the
39+
// current context as observed by a dependent coroutine can change
40+
// arbitrarily during a yield/await. Dependent coroutines are so-named
41+
// because they depend on their senders to enter the appropriate context
42+
// before each send. Coroutines and generators are dependent by default
43+
// for backwards compatibility.
44+
// * The purpose of the context chain is to enable *independent* coroutines
45+
// and generators, which have their own context stacks. Whenever an
46+
// independent coroutine starts or resumes execution, the current context
47+
// automatically switches to the context associated with the coroutine.
48+
// This is accomplished by linking the coroutine's chain link (at
49+
// PyGenObject::_ctx_chain) to the head of the thread's chain. Independent
50+
// coroutines are so-named because they do not depend on their senders to
51+
// enter the appropriate context before each send.
52+
// * The head link is unlinked from the thread's chain when its associated
53+
// independent coroutine or generator stops executing (yields, awaits,
54+
// returns, or throws).
55+
// * A running dependent coroutine's chain link is linked into the thread's
56+
// chain if the coroutine is upgraded from dependent to independent by
57+
// assigning a context to the coroutine's `_context` property. The chain
58+
// link is inserted at the position corresponding to the coroutine's
59+
// position in the call chain relative to any other currently running
60+
// independent coroutines. For example, if dependent coroutine `coro_a`
61+
// calls function `func_b` which resumes independent coroutine `coro_c`
62+
// which assigns a context to `coro_a._context`, then `coro_a` becomes an
63+
// independent coroutine with its chain link inserted after `coro_c`'s
64+
// chain link (which remains the head link).
65+
// * A running independent coroutine's chain link is unlinked from the
66+
// thread's chain if the coroutine is downgraded from independent to
67+
// dependent by assigning `None` to its `_context` property.
68+
// * The references to the object at the `prev` link in the chain are
69+
// implicit (borrowed).
70+
typedef struct _PyContextChain {
71+
// NULL for dependent coroutines/generators, non-NULL for independent
72+
// coroutines/generators.
73+
PyObject *ctx;
74+
// NULL if unlinked from the thread's context chain, non-NULL otherwise.
75+
struct _PyContextChain *prev;
76+
} _PyContextChain;
77+
78+
79+
#endif /* !Py_INTERNAL_CONTEXTCHAIN_H */

Include/internal/pycore_genobject.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#ifndef Py_INTERNAL_GENOBJECT_H
22
#define Py_INTERNAL_GENOBJECT_H
3+
4+
#include "pycore_contextchain.h" // _PyContextChain
5+
36
#ifdef __cplusplus
47
extern "C" {
58
#endif
@@ -22,6 +25,7 @@ extern "C" {
2225
PyObject *prefix##_qualname; \
2326
_PyErr_StackItem prefix##_exc_state; \
2427
PyObject *prefix##_origin_or_finalizer; \
28+
_PyContextChain _ctx_chain; \
2529
char prefix##_hooks_inited; \
2630
char prefix##_closed; \
2731
char prefix##_running_async; \

Include/internal/pycore_tstate.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern "C" {
99
#endif
1010

1111
#include "pycore_brc.h" // struct _brc_thread_state
12+
#include "pycore_contextchain.h" // _PyContextChain
1213
#include "pycore_freelist_state.h" // struct _Py_freelists
1314
#include "pycore_mimalloc.h" // struct _mimalloc_thread_state
1415
#include "pycore_qsbr.h" // struct qsbr
@@ -21,6 +22,9 @@ typedef struct _PyThreadStateImpl {
2122
// semi-public fields are in PyThreadState.
2223
PyThreadState base;
2324

25+
// Lazily initialized (must be zeroed at startup).
26+
_PyContextChain _ctx_chain;
27+
2428
PyObject *asyncio_running_loop; // Strong reference
2529

2630
struct _qsbr_thread_state *qsbr; // only used by free-threaded build

0 commit comments

Comments
 (0)