Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Doc/howto/free-threading-python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ is set to true by default which causes threads created with
:class:`threading.Thread` to start with a copy of the
:class:`~contextvars.Context()` of the caller of
:meth:`~threading.Thread.start`. In the default GIL-enabled build, the flag
defaults to false so threads start with an
empty :class:`~contextvars.Context()`.
defaults to false, so threads start with a context containing only the caller's
current bindings for context variables created by
:meth:`~contextvars.ContextVar.thread_inheritable`.


Warning filters
Expand Down
66 changes: 66 additions & 0 deletions Doc/library/contextvars.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,72 @@ Context Variables
:class:`!ContextVar`\s are :ref:`generic <generics>` over the type of
their contained value.

.. classmethod:: ContextVar.thread_inheritable(name, [*, default])

Return a new context variable whose binding is inherited by new
:class:`threading.Thread` instances. When
:meth:`threading.Thread.start` would otherwise start the thread with
an empty context (that is, when
:data:`sys.flags.thread_inherit_context` is false and no explicit
:class:`Context` was supplied), the new thread's context is initialized
with the caller's current bindings for all thread-inheritable variables.
This allows individual context variables to opt in to thread inheritance
on a per-variable basis.

These are ordinary bindings in the new thread's context: they are
visible to :func:`copy_context`, may be changed independently with
:meth:`ContextVar.set`, and are in turn inherited by threads started
from the new thread. Threads started with an explicitly supplied
context are unaffected, as are threads started by means other than
:meth:`threading.Thread.start`, such as
:func:`_thread.start_new_thread` or the C API.

The *name* and *default* parameters have the same meaning as for the
:class:`ContextVar` constructor. When
:data:`sys.flags.thread_inherit_context` is true, a variable created
with this method behaves identically to one created with
:class:`ContextVar`, so it is safe to use unconditionally.

Libraries that also support Python versions without this method must
choose how to degrade on those versions. For state with a safe
default in a new thread (the behavior that :class:`threading.local`
based state has always had), fall back to an ordinary context
variable::

_new_var = getattr(ContextVar, "thread_inheritable", ContextVar)
var = _new_var("var")

With this fallback, on older versions new threads see the variable's
default rather than the starting thread's binding, unless the
application enables :option:`-X thread_inherit_context <-X>`.

For state that was previously a module-level global, reverting to the
default in new threads may break existing threaded code, since such
code has always seen the current global value. Those libraries can
instead pick the best semantics each interpreter supports::

if hasattr(ContextVar, "thread_inheritable"):
# Context-local and inherited by new threads.
_new_var = ContextVar.thread_inheritable
elif getattr(sys.flags, "thread_inherit_context", False):
# Threads inherit the whole context, so an ordinary
# context variable is inherited too.
_new_var = ContextVar
else:
# Keep the library's historical global semantics.
_new_var = _GlobalVar

Here ``_GlobalVar`` is a class provided by the library that stores a
single process-wide value. To be substitutable for
:class:`ContextVar` it must accept the same constructor arguments
(*name* positional, keyword-only *default*), distinguish a missing
*default* from ``default=None``, and provide ``get()``, ``set()``
returning a token, and ``reset()``. This case gives up task-local
isolation: concurrent tasks and threads share one value, exactly as
they did before the library adopted context variables.

.. versionadded:: 3.16

.. attribute:: ContextVar.name

The name of the variable. This is a read-only property.
Expand Down
6 changes: 4 additions & 2 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,10 @@ since it is impossible to detect the termination of alien threads.
the thread. The default value is ``None`` which indicates that the
:data:`sys.flags.thread_inherit_context` flag controls the behaviour. If
the flag is true, threads will start with a copy of the context of the
caller of :meth:`~Thread.start`. If false, they will start with an empty
context. To explicitly start with an empty context, pass a new instance of
caller of :meth:`~Thread.start`. If false, they will start with a
context containing only the caller's current bindings for context variables
created by :meth:`~contextvars.ContextVar.thread_inheritable`. To explicitly
start with an empty context, pass a new instance of
:class:`~contextvars.Context()`. To explicitly start with a copy of the
current context, pass the value from :func:`~contextvars.copy_context`. The
flag defaults true on free-threaded builds and false otherwise.
Expand Down
21 changes: 12 additions & 9 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -675,11 +675,12 @@ Miscellaneous options
.. versionadded:: 3.13

* :samp:`-X thread_inherit_context={0,1}` causes :class:`~threading.Thread`
to, by default, use a copy of context of the caller of
``Thread.start()`` when starting. Otherwise, threads will start
with an empty context. If unset, the value of this option defaults
to ``1`` on free-threaded builds and to ``0`` otherwise. See also
:envvar:`PYTHON_THREAD_INHERIT_CONTEXT`.
to, by default, use a copy of the context of the caller of
``Thread.start()`` when starting. Otherwise, threads start with a context
containing only the caller's current bindings for context variables
created by :meth:`~contextvars.ContextVar.thread_inheritable`. If unset, the
value of this option defaults to ``1`` on free-threaded builds and to ``0``
otherwise. See also :envvar:`PYTHON_THREAD_INHERIT_CONTEXT`.

.. versionadded:: 3.14

Expand Down Expand Up @@ -1367,10 +1368,12 @@ conflict.
.. envvar:: PYTHON_THREAD_INHERIT_CONTEXT

If this variable is set to ``1`` then :class:`~threading.Thread` will,
by default, use a copy of context of the caller of ``Thread.start()``
when starting. Otherwise, new threads will start with an empty context.
If unset, this variable defaults to ``1`` on free-threaded builds and to
``0`` otherwise. See also :option:`-X thread_inherit_context<-X>`.
by default, use a copy of the context of the caller of ``Thread.start()``
when starting. Otherwise, new threads start with a context containing only
the caller's current bindings for context variables created by
:meth:`~contextvars.ContextVar.thread_inheritable`. If unset, this variable
defaults to ``1`` on free-threaded builds and to ``0`` otherwise. See also
:option:`-X thread_inherit_context<-X>`.

.. versionadded:: 3.14

Expand Down
7 changes: 7 additions & 0 deletions Include/internal/pycore_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ struct _pycontextobject {
PyHamtObject *ctx_vars;
PyObject *ctx_weakreflist;
int ctx_entered;
// Redundant subset of ctx_vars holding only the bindings of
// thread-inheritable context variables (see
// ContextVar.thread_inheritable()). Used to efficiently create the
// starting context of a new thread.
PyHamtObject *ctx_thread_inheritable_vars;
};


struct _pycontextvarobject {
PyObject_HEAD
PyObject *var_name;
PyObject *var_default;
char var_thread_inheritable;
#ifndef Py_GIL_DISABLED
PyObject *var_cached;
uint64_t var_cached_tsid;
Expand All @@ -54,6 +60,7 @@ struct _pycontexttokenobject {
// _testinternalcapi.hamt() used by tests.
// Export for '_testcapi' shared extension
PyAPI_FUNC(PyObject*) _PyContext_NewHamtForTests(void);
PyAPI_FUNC(PyObject*) _PyContext_NewThreadStartContext(void);

PyAPI_FUNC(int) _PyContext_Enter(PyThreadState *ts, PyObject *octx);
PyAPI_FUNC(int) _PyContext_Exit(PyThreadState *ts, PyObject *octx);
Expand Down
Loading
Loading