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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy