Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add thread-safe context manager to "warnings" module #128300

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading