Skip to content

Commit 902df7c

Browse files
committed
Add 'context' parameter to Thread.
By default, inherit the context from the thread calling `Thread.start()`.
1 parent deb659d commit 902df7c

File tree

4 files changed

+79
-6
lines changed

4 files changed

+79
-6
lines changed

Doc/library/threading.rst

Lines changed: 9 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="inherit")
338338

339339
This constructor should always be called with keyword arguments. Arguments
340340
are:
@@ -359,6 +359,10 @@ 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 `contextvars.Context` value to use while running the thread.
363+
The default is to inherit the context of the caller of :meth:`~Thread.start`.
364+
If set to ``None``, the context will be empty.
365+
362366
If the subclass overrides the constructor, it must make sure to invoke the
363367
base class constructor (``Thread.__init__()``) before doing anything else to
364368
the thread.
@@ -369,6 +373,10 @@ since it is impossible to detect the termination of alien threads.
369373
.. versionchanged:: 3.10
370374
Use the *target* name if *name* argument is omitted.
371375

376+
.. versionchanged:: 3.14
377+
Added the *context* parameter. Previously threads always ran with an empty
378+
context.
379+
372380
.. method:: start()
373381

374382
Start the thread's activity.

Lib/test/test_context.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,49 @@ def sub(num):
383383
tp.shutdown()
384384
self.assertEqual(results, list(range(10)))
385385

386+
@isolated_context
387+
@threading_helper.requires_working_threading()
388+
def test_context_thread_inherit(self):
389+
import threading
390+
391+
cvar = contextvars.ContextVar('cvar')
392+
393+
# By default, the context of the caller is inheritied
394+
def run_inherit():
395+
self.assertEqual(cvar.get(), 1)
396+
397+
cvar.set(1)
398+
thread = threading.Thread(target=run_inherit)
399+
thread.start()
400+
thread.join()
401+
402+
# If context=None is passed, the thread has an empty context
403+
def run_empty():
404+
with self.assertRaises(LookupError):
405+
cvar.get()
406+
407+
thread = threading.Thread(target=run_empty, context=None)
408+
thread.start()
409+
thread.join()
410+
411+
# An explicit Context value can also be passed
412+
custom_ctx = contextvars.Context()
413+
custom_var = None
414+
415+
def setup_context():
416+
nonlocal custom_var
417+
custom_var = contextvars.ContextVar('custom')
418+
custom_var.set(2)
419+
420+
custom_ctx.run(setup_context)
421+
422+
def run_custom():
423+
self.assertEqual(custom_var.get(), 2)
424+
425+
thread = threading.Thread(target=run_custom, context=custom_ctx)
426+
thread.start()
427+
thread.join()
428+
386429

387430
# HAMT Tests
388431

Lib/test/test_decimal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,8 +1725,8 @@ def test_threading(self):
17251725
self.finish1 = threading.Event()
17261726
self.finish2 = threading.Event()
17271727

1728-
th1 = threading.Thread(target=thfunc1, args=(self,))
1729-
th2 = threading.Thread(target=thfunc2, args=(self,))
1728+
th1 = threading.Thread(target=thfunc1, args=(self,), context=None)
1729+
th2 = threading.Thread(target=thfunc2, args=(self,), context=None)
17301730

17311731
th1.start()
17321732
th2.start()

Lib/threading.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import sys as _sys
55
import _thread
66
import warnings
7+
import contextvars as _contextvars
8+
79

810
from time import monotonic as _time
911
from _weakrefset import WeakSet
@@ -871,7 +873,7 @@ class Thread:
871873
_initialized = False
872874

873875
def __init__(self, group=None, target=None, name=None,
874-
args=(), kwargs=None, *, daemon=None):
876+
args=(), kwargs=None, *, daemon=None, context='inherit'):
875877
"""This constructor should always be called with keyword arguments. Arguments are:
876878
877879
*group* should be None; reserved for future extension when a ThreadGroup
@@ -888,6 +890,10 @@ class is implemented.
888890
*kwargs* is a dictionary of keyword arguments for the target
889891
invocation. Defaults to {}.
890892
893+
*context* is the contextvars.Context value to use for the thread. The default
894+
is to inherit the context of the caller. Set to None to start with an empty
895+
context.
896+
891897
If a subclass overrides the constructor, it must make sure to invoke
892898
the base class constructor (Thread.__init__()) before doing anything
893899
else to the thread.
@@ -917,6 +923,7 @@ class is implemented.
917923
self._daemonic = daemon
918924
else:
919925
self._daemonic = current_thread().daemon
926+
self._context = context
920927
self._ident = None
921928
if _HAVE_THREAD_NATIVE_ID:
922929
self._native_id = None
@@ -972,9 +979,15 @@ def start(self):
972979

973980
with _active_limbo_lock:
974981
_limbo[self] = self
982+
983+
if self._context == 'inherit':
984+
# No context provided, inherit the context of the caller.
985+
self._context = _contextvars.copy_context()
986+
975987
try:
976988
# Start joinable thread
977-
_start_joinable_thread(self._bootstrap, handle=self._handle,
989+
_start_joinable_thread(self._bootstrap,
990+
handle=self._handle,
978991
daemon=self.daemon)
979992
except Exception:
980993
with _active_limbo_lock:
@@ -1050,8 +1063,17 @@ def _bootstrap_inner(self):
10501063
if _profile_hook:
10511064
_sys.setprofile(_profile_hook)
10521065

1066+
if self._context is None:
1067+
# Run with empty context, matching behaviour of
1068+
# threading.local and older versions of Python.
1069+
run = self.run
1070+
else:
1071+
# Run with the provided or the inherited context.
1072+
def run():
1073+
self._context.run(self.run)
1074+
10531075
try:
1054-
self.run()
1076+
run()
10551077
except:
10561078
self._invoke_excepthook(self)
10571079
finally:

0 commit comments

Comments
 (0)