Skip to content

Commit 6d00c2a

Browse files
committed
Add 'context' parameter to Thread.
* Add ``sys.flags.inherit_context``. * Add ``-X inherit_context`` and :envvar:`PYTHON_INHERIT_CONTEXT`
1 parent ba99f2e commit 6d00c2a

File tree

13 files changed

+216
-12
lines changed

13 files changed

+216
-12
lines changed

Doc/library/sys.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,8 @@ always available. Unless explicitly noted otherwise, all variables are read-only
535535
.. data:: flags
536536

537537
The :term:`named tuple` *flags* exposes the status of command line
538-
flags. The attributes are read only.
538+
flags. Flags should only be accessed only by name and not by index. The
539+
attributes are read only.
539540

540541
.. list-table::
541542

@@ -594,6 +595,12 @@ always available. Unless explicitly noted otherwise, all variables are read-only
594595
* - .. attribute:: flags.warn_default_encoding
595596
- :option:`-X warn_default_encoding <-X>`
596597

598+
* - .. attribute:: flags.gil
599+
- :option:`-X gil <-X>` and :envvar:`PYTHON_GIL`
600+
601+
* - .. attribute:: flags.inherit_context
602+
- :option:`-X inherit_context <-X>` and :envvar:`PYTHON_INHERIT_CONTEXT`
603+
597604
.. versionchanged:: 3.2
598605
Added ``quiet`` attribute for the new :option:`-q` flag.
599606

@@ -620,6 +627,12 @@ always available. Unless explicitly noted otherwise, all variables are read-only
620627
.. versionchanged:: 3.11
621628
Added the ``int_max_str_digits`` attribute.
622629

630+
.. versionchanged:: 3.13
631+
Added the ``gil`` attribute.
632+
633+
.. versionchanged:: 3.14
634+
Added the ``inherit_context`` attribute.
635+
623636

624637
.. data:: float_info
625638

Doc/library/threading.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ since it is impossible to detect the termination of alien threads.
334334

335335

336336
.. class:: Thread(group=None, target=None, name=None, args=(), kwargs={}, *, \
337-
daemon=None)
337+
daemon=None, context=None)
338338

339339
This constructor should always be called with keyword arguments. Arguments
340340
are:
@@ -359,6 +359,17 @@ since it is impossible to detect the termination of alien threads.
359359
If ``None`` (the default), the daemonic property is inherited from the
360360
current thread.
361361

362+
*context* is the :class:`~contextvars.Context` value to use when starting
363+
the thread. The default value is ``None`` which indicates that the
364+
:data:`sys.flags.inherit_context` flag controls the behaviour. If
365+
the flag is true, threads will start with a copy of the context of the
366+
caller of :meth:`~Thread.start`. If false, they will start with
367+
an empty context. To explicitly start with an empty context,
368+
pass a new instance of :class:`~contextvars.Context()`. To explicitly
369+
start with a copy of the current context, pass the value from
370+
:func:`~contextvars.copy_context()`. The flag defaults true on
371+
free-threaded builds and false otherwise.
372+
362373
If the subclass overrides the constructor, it must make sure to invoke the
363374
base class constructor (``Thread.__init__()``) before doing anything else to
364375
the thread.
@@ -369,6 +380,9 @@ since it is impossible to detect the termination of alien threads.
369380
.. versionchanged:: 3.10
370381
Use the *target* name if *name* argument is omitted.
371382

383+
.. versionchanged:: 3.14
384+
Added the *context* parameter.
385+
372386
.. method:: start()
373387

374388
Start the thread's activity.

Doc/using/cmdline.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,15 @@ Miscellaneous options
628628

629629
.. versionadded:: 3.13
630630

631+
* :samp:`-X inherit_context={0,1}` causes :class:`~threading.Thread`
632+
to, by default, use a copy of context of of the caller of
633+
``Thread.start()`` when starting. Otherwise, threads will start
634+
with an empty context. If unset, the value of this option defaults
635+
to ``1`` on free-threaded builds and to ``0`` otherwise. See also
636+
:envvar:`PYTHON_INHERIT_CONTEXT`.
637+
638+
.. versionadded:: 3.14
639+
631640
It also allows passing arbitrary values and retrieving them through the
632641
:data:`sys._xoptions` dictionary.
633642

@@ -1221,6 +1230,16 @@ conflict.
12211230

12221231
.. versionadded:: 3.13
12231232

1233+
.. envvar:: PYTHON_INHERIT_CONTEXT
1234+
1235+
If this variable is set to ``1`` then :class:`~threading.Thread` will,
1236+
by default, use a copy of context of of the caller of ``Thread.start()``
1237+
when starting. Otherwise, new threads will start with an empty context.
1238+
If unset, this variable defaults to ``1`` on free-threaded builds and to
1239+
``0`` otherwise. See also :option:`-X inherit_context<-X>`.
1240+
1241+
.. versionadded:: 3.14
1242+
12241243
Debug-mode variables
12251244
~~~~~~~~~~~~~~~~~~~~
12261245

Include/cpython/initconfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ typedef struct PyConfig {
179179
int use_frozen_modules;
180180
int safe_path;
181181
int int_max_str_digits;
182+
int inherit_context;
182183
#ifdef __APPLE__
183184
int use_system_logger;
184185
#endif

Lib/test/test_capi/test_config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_config_get(self):
5555
("filesystem_errors", str, None),
5656
("hash_seed", int, None),
5757
("home", str | None, None),
58+
("inherit_context", int, None),
5859
("import_time", bool, None),
5960
("inspect", bool, None),
6061
("install_signal_handlers", bool, None),
@@ -98,7 +99,7 @@ def test_config_get(self):
9899
]
99100
if support.Py_DEBUG:
100101
options.append(("run_presite", str | None, None))
101-
if sysconfig.get_config_var('Py_GIL_DISABLED'):
102+
if support.Py_GIL_DISABLED:
102103
options.append(("enable_gil", int, None))
103104
options.append(("tlbc_enabled", int, None))
104105
if support.MS_WINDOWS:
@@ -170,7 +171,7 @@ def test_config_get_sys_flags(self):
170171
("warn_default_encoding", "warn_default_encoding", False),
171172
("safe_path", "safe_path", False),
172173
("int_max_str_digits", "int_max_str_digits", False),
173-
# "gil" is tested below
174+
# "gil" and "inherit_context" are tested below
174175
):
175176
with self.subTest(flag=flag, name=name, negate=negate):
176177
value = config_get(name)
@@ -182,11 +183,14 @@ def test_config_get_sys_flags(self):
182183
config_get('use_hash_seed') == 0
183184
or config_get('hash_seed') != 0)
184185

185-
if sysconfig.get_config_var('Py_GIL_DISABLED'):
186+
if support.Py_GIL_DISABLED:
186187
value = config_get('enable_gil')
187188
expected = (value if value != -1 else None)
188189
self.assertEqual(sys.flags.gil, expected)
189190

191+
expected_inherit_context = 1 if support.Py_GIL_DISABLED else 0
192+
self.assertEqual(sys.flags.inherit_context, expected_inherit_context)
193+
190194
def test_config_get_non_existent(self):
191195
# Test PyConfig_Get() on non-existent option name
192196
config_get = _testcapi.config_get

Lib/test/test_context.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import collections.abc
23
import concurrent.futures
34
import contextvars
@@ -383,6 +384,60 @@ def sub(num):
383384
tp.shutdown()
384385
self.assertEqual(results, list(range(10)))
385386

387+
@isolated_context
388+
@threading_helper.requires_working_threading()
389+
def test_context_thread_inherit(self):
390+
import threading
391+
392+
cvar = contextvars.ContextVar('cvar')
393+
394+
def run_context_none():
395+
if sys.flags.inherit_context:
396+
expected = 1
397+
else:
398+
expected = None
399+
self.assertEqual(cvar.get(None), expected)
400+
401+
# By default, context is inherited based on the
402+
# sys.flags.inherit_context option.
403+
cvar.set(1)
404+
thread = threading.Thread(target=run_context_none)
405+
thread.start()
406+
thread.join()
407+
408+
# Passing 'None' explicitly should have same behaviour as not
409+
# passing parameter.
410+
thread = threading.Thread(target=run_context_none, context=None)
411+
thread.start()
412+
thread.join()
413+
414+
# An explicit Context value can also be passed
415+
custom_ctx = contextvars.Context()
416+
custom_var = None
417+
418+
def setup_context():
419+
nonlocal custom_var
420+
custom_var = contextvars.ContextVar('custom')
421+
custom_var.set(2)
422+
423+
custom_ctx.run(setup_context)
424+
425+
def run_custom():
426+
self.assertEqual(custom_var.get(), 2)
427+
428+
thread = threading.Thread(target=run_custom, context=custom_ctx)
429+
thread.start()
430+
thread.join()
431+
432+
# You can also pass a new Context() object to start with an empty context
433+
def run_empty():
434+
with self.assertRaises(LookupError):
435+
cvar.get()
436+
437+
thread = threading.Thread(target=run_empty, context=contextvars.Context())
438+
thread.start()
439+
thread.join()
440+
386441

387442
# HAMT Tests
388443

Lib/test/test_decimal.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import random
4545
import inspect
4646
import threading
47+
import contextvars
4748

4849

4950
if sys.platform == 'darwin':
@@ -1725,8 +1726,10 @@ def test_threading(self):
17251726
self.finish1 = threading.Event()
17261727
self.finish2 = threading.Event()
17271728

1728-
th1 = threading.Thread(target=thfunc1, args=(self,))
1729-
th2 = threading.Thread(target=thfunc2, args=(self,))
1729+
th1 = threading.Thread(target=thfunc1, args=(self,),
1730+
context=contextvars.Context())
1731+
th2 = threading.Thread(target=thfunc2, args=(self,),
1732+
context=contextvars.Context())
17301733

17311734
th1.start()
17321735
th2.start()

Lib/test/test_embed.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
INIT_LOOPS = 4
5151
MAX_HASH_SEED = 4294967295
5252

53-
ABI_THREAD = 't' if sysconfig.get_config_var('Py_GIL_DISABLED') else ''
53+
ABI_THREAD = 't' if support.Py_GIL_DISABLED else ''
5454
# PLATSTDLIB_LANDMARK copied from Modules/getpath.py
5555
if os.name == 'nt':
5656
PLATSTDLIB_LANDMARK = f'{sys.platlibdir}'
@@ -60,6 +60,10 @@
6060
PLATSTDLIB_LANDMARK = (f'{sys.platlibdir}/python{VERSION_MAJOR}.'
6161
f'{VERSION_MINOR}{ABI_THREAD}/lib-dynload')
6262

63+
if support.Py_GIL_DISABLED:
64+
DEFAULT_INHERIT_CONTEXT = 1
65+
else:
66+
DEFAULT_INHERIT_CONTEXT = 0
6367

6468
# If we are running from a build dir, but the stdlib has been installed,
6569
# some tests need to expect different results.
@@ -586,6 +590,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
586590
'tracemalloc': 0,
587591
'perf_profiling': 0,
588592
'import_time': False,
593+
'inherit_context': DEFAULT_INHERIT_CONTEXT,
589594
'code_debug_ranges': True,
590595
'show_ref_count': False,
591596
'dump_refs': False,

Lib/test/test_sys.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,8 +1845,9 @@ def test_pythontypes(self):
18451845
# symtable entry
18461846
# XXX
18471847
# sys.flags
1848-
# FIXME: The +1 will not be necessary once gh-122575 is fixed
1849-
check(sys.flags, vsize('') + self.P * (1 + len(sys.flags)))
1848+
# FIXME: The +2 is for the 'gil' and 'inherit_context' flags and
1849+
# will not be necessary once gh-122575 is fixed
1850+
check(sys.flags, vsize('') + self.P * (2 + len(sys.flags)))
18501851

18511852
def test_asyncgen_hooks(self):
18521853
old = sys.get_asyncgen_hooks()

Lib/threading.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os as _os
44
import sys as _sys
55
import _thread
6+
import _contextvars
67

78
from time import monotonic as _time
89
from _weakrefset import WeakSet
@@ -871,7 +872,7 @@ class Thread:
871872
_initialized = False
872873

873874
def __init__(self, group=None, target=None, name=None,
874-
args=(), kwargs=None, *, daemon=None):
875+
args=(), kwargs=None, *, daemon=None, context=None):
875876
"""This constructor should always be called with keyword arguments. Arguments are:
876877
877878
*group* should be None; reserved for future extension when a ThreadGroup
@@ -888,6 +889,14 @@ class is implemented.
888889
*kwargs* is a dictionary of keyword arguments for the target
889890
invocation. Defaults to {}.
890891
892+
*context* is the contextvars.Context value to use for the thread.
893+
The default value is None, which means to check
894+
sys.flags.inherit_context. If that flag is true, use a copy of
895+
the context of the caller. If false, use an empty context. To
896+
explicitly start with an empty context, pass a new instance of
897+
contextvars.Context(). To explicitly start with a copy of the
898+
current context, pass the value from contextvars.copy_context().
899+
891900
If a subclass overrides the constructor, it must make sure to invoke
892901
the base class constructor (Thread.__init__()) before doing anything
893902
else to the thread.
@@ -917,6 +926,7 @@ class is implemented.
917926
self._daemonic = daemon
918927
else:
919928
self._daemonic = current_thread().daemon
929+
self._context = context
920930
self._ident = None
921931
if _HAVE_THREAD_NATIVE_ID:
922932
self._native_id = None
@@ -972,6 +982,16 @@ def start(self):
972982

973983
with _active_limbo_lock:
974984
_limbo[self] = self
985+
986+
if self._context is None:
987+
# No context provided
988+
if _sys.flags.inherit_context:
989+
# start with a copy of the context of the caller
990+
self._context = _contextvars.copy_context()
991+
else:
992+
# start with an empty context
993+
self._context = _contextvars.Context()
994+
975995
try:
976996
# Start joinable thread
977997
_start_joinable_thread(self._bootstrap, handle=self._handle,
@@ -1051,7 +1071,7 @@ def _bootstrap_inner(self):
10511071
_sys.setprofile(_profile_hook)
10521072

10531073
try:
1054-
self.run()
1074+
self._context.run(self.run)
10551075
except:
10561076
self._invoke_excepthook(self)
10571077
finally:

0 commit comments

Comments
 (0)