Skip to content

Commit 708127d

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

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)
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