Skip to content

Commit

Permalink
Add 'context' parameter to Thread.
Browse files Browse the repository at this point in the history
By default, inherit the context from the thread calling
`Thread.start()`.
  • Loading branch information
nascheme committed Dec 27, 2024
1 parent a269d18 commit 1d7ded0
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 6 deletions.
10 changes: 9 additions & 1 deletion Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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="inherit")

This constructor should always be called with keyword arguments. Arguments
are:
Expand All @@ -359,6 +359,10 @@ 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 `contextvars.Context` value to use while running the thread.
The default is to inherit the context of the caller of :meth:`~Thread.start`.
If set to ``None``, the context will be empty.

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.
Expand All @@ -369,6 +373,10 @@ 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
Added the *context* parameter. Previously threads always ran with an empty
context.

.. method:: start()

Start the thread's activity.
Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,49 @@ 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, the thread has an empty context
def run_empty():
with self.assertRaises(LookupError):
cvar.get()

thread = threading.Thread(target=run_empty, 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()


# HAMT Tests

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,8 +1725,8 @@ 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=None)
th2 = threading.Thread(target=thfunc2, args=(self,), context=None)

th1.start()
th2.start()
Expand Down
28 changes: 25 additions & 3 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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='inherit'):
"""This constructor should always be called with keyword arguments. Arguments are:
*group* should be None; reserved for future extension when a ThreadGroup
Expand All @@ -888,6 +890,10 @@ 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
is to inherit the context of the caller. Set to None to start with an empty
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.
Expand Down Expand Up @@ -917,6 +923,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
Expand Down Expand Up @@ -972,9 +979,15 @@ def start(self):

with _active_limbo_lock:
_limbo[self] = self

if self._context == 'inherit':
# No context provided, inherit 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:
Expand Down Expand Up @@ -1050,8 +1063,17 @@ def _bootstrap_inner(self):
if _profile_hook:
_sys.setprofile(_profile_hook)

if self._context is None:
# Run with empty context, matching behaviour of
# threading.local and older versions of Python.
run = self.run
else:
# Run with the provided or the inherited context.
def run():
self._context.run(self.run)

try:
self.run()
run()
except:
self._invoke_excepthook(self)
finally:
Expand Down

0 comments on commit 1d7ded0

Please sign in to comment.