diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index f183f3f535c4cb..91af9bff615d03 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -334,7 +334,7 @@ since it is impossible to detect the termination of alien threads. .. class:: Thread(group=None, target=None, name=None, args=(), kwargs={}, *, \ - daemon=None) + daemon=None, context=None) This constructor should always be called with keyword arguments. Arguments are: @@ -359,6 +359,11 @@ since it is impossible to detect the termination of alien threads. If ``None`` (the default), the daemonic property is inherited from the current thread. + *context* is the :class:`~contextvars.Context` value to use while running + the thread. The default value is ``None`` which means to use a copy + of the context of the caller of :meth:`~Thread.start`. To start with + an empty context, pass a new instance of :class:`~contextvars.Context` + If the subclass overrides the constructor, it must make sure to invoke the base class constructor (``Thread.__init__()``) before doing anything else to the thread. @@ -369,6 +374,12 @@ since it is impossible to detect the termination of alien threads. .. versionchanged:: 3.10 Use the *target* name if *name* argument is omitted. + .. versionchanged:: 3.14 + Threads now inherit the context of the caller of :meth:`~Thread.start` + instead of starting with an empty context. The *context* parameter + was added. Pass a new :class:`~contextvars.Context()` if an empty context + is required. + .. method:: start() Start the thread's activity. diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py index 82d1797ab3b79e..55704f20afcba4 100644 --- a/Lib/test/test_context.py +++ b/Lib/test/test_context.py @@ -383,6 +383,54 @@ def sub(num): tp.shutdown() self.assertEqual(results, list(range(10))) + @isolated_context + @threading_helper.requires_working_threading() + def test_context_thread_inherit(self): + import threading + + cvar = contextvars.ContextVar('cvar') + + # By default, the context of the caller is inheritied + def run_inherit(): + self.assertEqual(cvar.get(), 1) + + cvar.set(1) + thread = threading.Thread(target=run_inherit) + thread.start() + thread.join() + + # If context=None is passed, behaviour is to inherit + thread = threading.Thread(target=run_inherit, context=None) + thread.start() + thread.join() + + # An explicit Context value can also be passed + custom_ctx = contextvars.Context() + custom_var = None + + def setup_context(): + nonlocal custom_var + custom_var = contextvars.ContextVar('custom') + custom_var.set(2) + + custom_ctx.run(setup_context) + + def run_custom(): + self.assertEqual(custom_var.get(), 2) + + thread = threading.Thread(target=run_custom, context=custom_ctx) + thread.start() + thread.join() + + # You can also pass a new Context() object to start with an empty context + def run_empty(): + with self.assertRaises(LookupError): + cvar.get() + + thread = threading.Thread(target=run_empty, context=contextvars.Context()) + thread.start() + thread.join() + # HAMT Tests diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index bc6c6427740949..2e38bbfd4b93ea 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -44,6 +44,7 @@ import random import inspect import threading +import contextvars if sys.platform == 'darwin': @@ -1725,8 +1726,10 @@ def test_threading(self): self.finish1 = threading.Event() self.finish2 = threading.Event() - th1 = threading.Thread(target=thfunc1, args=(self,)) - th2 = threading.Thread(target=thfunc2, args=(self,)) + th1 = threading.Thread(target=thfunc1, args=(self,), + context=contextvars.Context()) + th2 = threading.Thread(target=thfunc2, args=(self,), + context=contextvars.Context()) th1.start() th2.start() diff --git a/Lib/threading.py b/Lib/threading.py index 78e591124278fc..b9c7492fdbadef 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -4,6 +4,8 @@ import sys as _sys import _thread import warnings +import contextvars as _contextvars + from time import monotonic as _time from _weakrefset import WeakSet @@ -871,7 +873,7 @@ class Thread: _initialized = False def __init__(self, group=None, target=None, name=None, - args=(), kwargs=None, *, daemon=None): + args=(), kwargs=None, *, daemon=None, context=None): """This constructor should always be called with keyword arguments. Arguments are: *group* should be None; reserved for future extension when a ThreadGroup @@ -888,6 +890,11 @@ class is implemented. *kwargs* is a dictionary of keyword arguments for the target invocation. Defaults to {}. + *context* is the contextvars.Context value to use for the thread. + The default value is None, which means to use a copy of the context + of the caller. To start with an empty context, pass a new instance + of contextvars.Context(). + If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread. @@ -917,6 +924,7 @@ class is implemented. self._daemonic = daemon else: self._daemonic = current_thread().daemon + self._context = context self._ident = None if _HAVE_THREAD_NATIVE_ID: self._native_id = None @@ -972,9 +980,15 @@ def start(self): with _active_limbo_lock: _limbo[self] = self + + if self._context is None: + # No context provided, inherit a copy of the context of the caller. + self._context = _contextvars.copy_context() + try: # Start joinable thread - _start_joinable_thread(self._bootstrap, handle=self._handle, + _start_joinable_thread(self._bootstrap, + handle=self._handle, daemon=self.daemon) except Exception: with _active_limbo_lock: @@ -1051,7 +1065,13 @@ def _bootstrap_inner(self): _sys.setprofile(_profile_hook) try: - self.run() + if self._context is None: + # Run with empty context, matching behaviour of + # threading.local and older versions of Python. + self.run() + else: + # Run with the provided or the inherited context. + self._context.run(self.run) except: self._invoke_excepthook(self) finally: diff --git a/Makefile.pre.in b/Makefile.pre.in index 67acf0fc520087..18484a42abb6e8 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -420,6 +420,7 @@ PARSER_HEADERS= \ # Python PYTHON_OBJS= \ + Python/_contextvars.o \ Python/_warnings.o \ Python/Python-ast.o \ Python/Python-tokenize.o \ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst new file mode 100644 index 00000000000000..f9d00cfbc9bcf6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst @@ -0,0 +1,4 @@ +Starting a new thread using :class:`threading.Thread` will now, by default, +use a copy of the :class:`contextvars.Context` from the caller of +:meth:`threading.Thread.start` rather than using an empty context. The +``_contextvars`` module is now built-in. diff --git a/Modules/Setup b/Modules/Setup index ddf39e0b966610..e01c7bb1a8a45e 100644 --- a/Modules/Setup +++ b/Modules/Setup @@ -132,7 +132,6 @@ PYTHONPATH=$(COREPYTHONPATH) #_asyncio _asynciomodule.c #_bisect _bisectmodule.c -#_contextvars _contextvarsmodule.c #_csv _csv.c #_datetime _datetimemodule.c #_decimal _decimal/_decimal.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 52c0f883d383db..189c30c558336e 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -31,7 +31,6 @@ @MODULE_ARRAY_TRUE@array arraymodule.c @MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c @MODULE__BISECT_TRUE@_bisect _bisectmodule.c -@MODULE__CONTEXTVARS_TRUE@_contextvars _contextvarsmodule.c @MODULE__CSV_TRUE@_csv _csv.c @MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c @MODULE__JSON_TRUE@_json _json.c diff --git a/Modules/config.c.in b/Modules/config.c.in index 53b4fb285498d0..41e42228c1ffd1 100644 --- a/Modules/config.c.in +++ b/Modules/config.c.in @@ -29,6 +29,7 @@ extern PyObject* PyInit__imp(void); extern PyObject* PyInit_gc(void); extern PyObject* PyInit__ast(void); extern PyObject* PyInit__tokenize(void); +extern PyObject* PyInit__contextvars(void); extern PyObject* _PyWarnings_Init(void); extern PyObject* PyInit__string(void); @@ -55,6 +56,9 @@ struct _inittab _PyImport_Inittab[] = { /* This lives in gcmodule.c */ {"gc", PyInit_gc}, + /* This lives in Python/_contextvars.c */ + {"_contextvars", PyInit__contextvars}, + /* This lives in _warnings.c */ {"_warnings", _PyWarnings_Init}, diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 9ebf58ae8a9bc4..ef6dbf9f8e4222 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -423,7 +423,6 @@ - @@ -570,6 +569,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 6c76a6ab592a84..b661aad2019454 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -1262,6 +1262,9 @@ PC + + Python + Python @@ -1526,9 +1529,6 @@ Objects - - Modules - Modules\zlib diff --git a/Modules/_contextvarsmodule.c b/Python/_contextvars.c similarity index 97% rename from Modules/_contextvarsmodule.c rename to Python/_contextvars.c index 3f96f07909b69a..0f8b8004c1af22 100644 --- a/Modules/_contextvarsmodule.c +++ b/Python/_contextvars.c @@ -1,6 +1,6 @@ #include "Python.h" -#include "clinic/_contextvarsmodule.c.h" +#include "clinic/_contextvars.c.h" /*[clinic input] module _contextvars diff --git a/Modules/clinic/_contextvarsmodule.c.h b/Python/clinic/_contextvars.c.h similarity index 100% rename from Modules/clinic/_contextvarsmodule.c.h rename to Python/clinic/_contextvars.c.h diff --git a/configure.ac b/configure.ac index badb19d55895de..3717cb44314609 100644 --- a/configure.ac +++ b/configure.ac @@ -7709,7 +7709,6 @@ dnl always enabled extension modules PY_STDLIB_MOD_SIMPLE([array]) PY_STDLIB_MOD_SIMPLE([_asyncio]) PY_STDLIB_MOD_SIMPLE([_bisect]) -PY_STDLIB_MOD_SIMPLE([_contextvars]) PY_STDLIB_MOD_SIMPLE([_csv]) PY_STDLIB_MOD_SIMPLE([_heapq]) PY_STDLIB_MOD_SIMPLE([_json])