mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Upgrade threading to 3.13.11; sys.setprofile & impl more threading (#6691)
* Upgrade threading,test_threading from 3.13.11 * increase ci limit to 45mins * impl more threading * no static for asyncgen
This commit is contained in:
1 parent
00bfea83bf
commit
69601a18a4
18 files changed
+2365
-756
No files matched your search
@@ -106,9 +106,7 @@ env:
|
||||
test_weakref
|
||||
test_yield_from
|
||||
ENV_POLLUTING_TESTS_COMMON: >-
|
||||
test_threading
|
||||
ENV_POLLUTING_TESTS_LINUX: >-
|
||||
test.test_concurrent_futures.test_thread_pool
|
||||
test.test_multiprocessing_fork.test_processes
|
||||
test.test_multiprocessing_fork.test_threads
|
||||
test.test_multiprocessing_forkserver.test_processes
|
||||
@@ -116,7 +114,6 @@ env:
|
||||
test.test_multiprocessing_spawn.test_processes
|
||||
test.test_multiprocessing_spawn.test_threads
|
||||
ENV_POLLUTING_TESTS_MACOS: >-
|
||||
test.test_concurrent_futures.test_thread_pool
|
||||
test.test_multiprocessing_forkserver.test_processes
|
||||
test.test_multiprocessing_forkserver.test_threads
|
||||
test.test_multiprocessing_spawn.test_processes
|
||||
@@ -297,21 +294,21 @@ jobs:
|
||||
env:
|
||||
RUSTPYTHON_SKIP_ENV_POLLUTERS: true
|
||||
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed --timeout 600 -v ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
timeout-minutes: 35
|
||||
timeout-minutes: 45
|
||||
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-dependent tests (Linux)
|
||||
env:
|
||||
RUSTPYTHON_SKIP_ENV_POLLUTERS: true
|
||||
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed --timeout 600 -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
timeout-minutes: 35
|
||||
timeout-minutes: 45
|
||||
|
||||
- if: runner.os == 'macOS'
|
||||
name: run cpython platform-dependent tests (MacOS)
|
||||
env:
|
||||
RUSTPYTHON_SKIP_ENV_POLLUTERS: true
|
||||
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed --timeout 600 -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
timeout-minutes: 35
|
||||
timeout-minutes: 45
|
||||
|
||||
- if: runner.os == 'Windows'
|
||||
name: run cpython platform-dependent tests (windows partial - fixme)
|
||||
|
||||
Vendored
+142
-7
@@ -11,15 +11,35 @@ Suggested usage is::
|
||||
import _dummy_thread as _thread
|
||||
|
||||
"""
|
||||
|
||||
# Exports only things specified by thread documentation;
|
||||
# skipping obsolete synonyms allocate(), start_new(), exit_thread().
|
||||
__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock',
|
||||
'interrupt_main', 'LockType', 'RLock',
|
||||
'_count']
|
||||
__all__ = [
|
||||
"error",
|
||||
"start_new_thread",
|
||||
"exit",
|
||||
"get_ident",
|
||||
"allocate_lock",
|
||||
"interrupt_main",
|
||||
"LockType",
|
||||
"RLock",
|
||||
"_count",
|
||||
"start_joinable_thread",
|
||||
"daemon_threads_allowed",
|
||||
"_shutdown",
|
||||
"_make_thread_handle",
|
||||
"_ThreadHandle",
|
||||
"_get_main_thread_ident",
|
||||
"_is_main_interpreter",
|
||||
"_local",
|
||||
]
|
||||
|
||||
# A dummy value
|
||||
TIMEOUT_MAX = 2**31
|
||||
|
||||
# Main thread ident for dummy implementation
|
||||
_MAIN_THREAD_IDENT = -1
|
||||
|
||||
# NOTE: this module can be imported early in the extension building process,
|
||||
# and so top level imports of other modules should be avoided. Instead, all
|
||||
# imports are done when needed on a function-by-function basis. Since threads
|
||||
@@ -27,6 +47,7 @@ TIMEOUT_MAX = 2**31
|
||||
|
||||
error = RuntimeError
|
||||
|
||||
|
||||
def start_new_thread(function, args, kwargs={}):
|
||||
"""Dummy implementation of _thread.start_new_thread().
|
||||
|
||||
@@ -52,6 +73,7 @@ def start_new_thread(function, args, kwargs={}):
|
||||
pass
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
_main = True
|
||||
global _interrupt
|
||||
@@ -59,10 +81,58 @@ def start_new_thread(function, args, kwargs={}):
|
||||
_interrupt = False
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def start_joinable_thread(function, handle=None, daemon=True):
|
||||
"""Dummy implementation of _thread.start_joinable_thread().
|
||||
|
||||
In dummy thread, we just run the function synchronously.
|
||||
"""
|
||||
if handle is None:
|
||||
handle = _ThreadHandle()
|
||||
try:
|
||||
function()
|
||||
except SystemExit:
|
||||
pass
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
handle._set_done()
|
||||
return handle
|
||||
|
||||
|
||||
def daemon_threads_allowed():
|
||||
"""Dummy implementation of _thread.daemon_threads_allowed()."""
|
||||
return True
|
||||
|
||||
|
||||
def _shutdown():
|
||||
"""Dummy implementation of _thread._shutdown()."""
|
||||
pass
|
||||
|
||||
|
||||
def _make_thread_handle(ident):
|
||||
"""Dummy implementation of _thread._make_thread_handle()."""
|
||||
handle = _ThreadHandle()
|
||||
handle._ident = ident
|
||||
return handle
|
||||
|
||||
|
||||
def _get_main_thread_ident():
|
||||
"""Dummy implementation of _thread._get_main_thread_ident()."""
|
||||
return _MAIN_THREAD_IDENT
|
||||
|
||||
|
||||
def _is_main_interpreter():
|
||||
"""Dummy implementation of _thread._is_main_interpreter()."""
|
||||
return True
|
||||
|
||||
|
||||
def exit():
|
||||
"""Dummy implementation of _thread.exit()."""
|
||||
raise SystemExit
|
||||
|
||||
|
||||
def get_ident():
|
||||
"""Dummy implementation of _thread.get_ident().
|
||||
|
||||
@@ -70,26 +140,31 @@ def get_ident():
|
||||
available, it is safe to assume that the current process is the
|
||||
only thread. Thus a constant can be safely returned.
|
||||
"""
|
||||
return -1
|
||||
return _MAIN_THREAD_IDENT
|
||||
|
||||
|
||||
def allocate_lock():
|
||||
"""Dummy implementation of _thread.allocate_lock()."""
|
||||
return LockType()
|
||||
|
||||
|
||||
def stack_size(size=None):
|
||||
"""Dummy implementation of _thread.stack_size()."""
|
||||
if size is not None:
|
||||
raise error("setting thread stack size not supported")
|
||||
return 0
|
||||
|
||||
|
||||
def _set_sentinel():
|
||||
"""Dummy implementation of _thread._set_sentinel()."""
|
||||
return LockType()
|
||||
|
||||
|
||||
def _count():
|
||||
"""Dummy implementation of _thread._count()."""
|
||||
return 0
|
||||
|
||||
|
||||
class LockType(object):
|
||||
"""Class implementing dummy implementation of _thread.LockType.
|
||||
|
||||
@@ -125,6 +200,7 @@ class LockType(object):
|
||||
else:
|
||||
if timeout > 0:
|
||||
import time
|
||||
|
||||
time.sleep(timeout)
|
||||
return False
|
||||
|
||||
@@ -153,14 +229,41 @@ class LockType(object):
|
||||
"locked" if self.locked_status else "unlocked",
|
||||
self.__class__.__module__,
|
||||
self.__class__.__qualname__,
|
||||
hex(id(self))
|
||||
hex(id(self)),
|
||||
)
|
||||
|
||||
|
||||
class _ThreadHandle:
|
||||
"""Dummy implementation of _thread._ThreadHandle."""
|
||||
|
||||
def __init__(self):
|
||||
self._ident = _MAIN_THREAD_IDENT
|
||||
self._done = False
|
||||
|
||||
@property
|
||||
def ident(self):
|
||||
return self._ident
|
||||
|
||||
def _set_done(self):
|
||||
self._done = True
|
||||
|
||||
def is_done(self):
|
||||
return self._done
|
||||
|
||||
def join(self, timeout=None):
|
||||
# In dummy thread, thread is always done
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<_ThreadHandle ident={self._ident}>"
|
||||
|
||||
|
||||
# Used to signal that interrupt_main was called in a "thread"
|
||||
_interrupt = False
|
||||
# True when not executing in a "thread"
|
||||
_main = True
|
||||
|
||||
|
||||
def interrupt_main():
|
||||
"""Set _interrupt flag to True to have start_new_thread raise
|
||||
KeyboardInterrupt upon exiting."""
|
||||
@@ -170,6 +273,7 @@ def interrupt_main():
|
||||
global _interrupt
|
||||
_interrupt = True
|
||||
|
||||
|
||||
class RLock:
|
||||
def __init__(self):
|
||||
self.locked_count = 0
|
||||
@@ -190,7 +294,7 @@ class RLock:
|
||||
return True
|
||||
|
||||
def locked(self):
|
||||
return self.locked_status != 0
|
||||
return self.locked_count != 0
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s.%s object owner=%s count=%s at %s>" % (
|
||||
@@ -199,5 +303,36 @@ class RLock:
|
||||
self.__class__.__qualname__,
|
||||
get_ident() if self.locked_count else 0,
|
||||
self.locked_count,
|
||||
hex(id(self))
|
||||
hex(id(self)),
|
||||
)
|
||||
|
||||
|
||||
class _local:
|
||||
"""Dummy implementation of _thread._local (thread-local storage)."""
|
||||
|
||||
def __init__(self):
|
||||
object.__setattr__(self, "_local__impl", {})
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name.startswith("_local__"):
|
||||
return object.__getattribute__(self, name)
|
||||
impl = object.__getattribute__(self, "_local__impl")
|
||||
try:
|
||||
return impl[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith("_local__"):
|
||||
return object.__setattr__(self, name, value)
|
||||
impl = object.__getattribute__(self, "_local__impl")
|
||||
impl[name] = value
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name.startswith("_local__"):
|
||||
return object.__delattr__(self, name)
|
||||
impl = object.__getattribute__(self, "_local__impl")
|
||||
try:
|
||||
del impl[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
Vendored
+21
-17
@@ -6,6 +6,7 @@ directly imported it would have made all subsequent imports succeed
|
||||
regardless of whether ``_thread`` was available which is not desired.
|
||||
|
||||
"""
|
||||
|
||||
from sys import modules as sys_modules
|
||||
|
||||
import _dummy_thread
|
||||
@@ -19,35 +20,38 @@ try:
|
||||
# Could have checked if ``_thread`` was not in sys.modules and gone
|
||||
# a different route, but decided to mirror technique used with
|
||||
# ``threading`` below.
|
||||
if '_thread' in sys_modules:
|
||||
held_thread = sys_modules['_thread']
|
||||
if "_thread" in sys_modules:
|
||||
held_thread = sys_modules["_thread"]
|
||||
holding_thread = True
|
||||
# Must have some module named ``_thread`` that implements its API
|
||||
# in order to initially import ``threading``.
|
||||
sys_modules['_thread'] = sys_modules['_dummy_thread']
|
||||
sys_modules["_thread"] = sys_modules["_dummy_thread"]
|
||||
|
||||
if 'threading' in sys_modules:
|
||||
if "threading" in sys_modules:
|
||||
# If ``threading`` is already imported, might as well prevent
|
||||
# trying to import it more than needed by saving it if it is
|
||||
# already imported before deleting it.
|
||||
held_threading = sys_modules['threading']
|
||||
held_threading = sys_modules["threading"]
|
||||
holding_threading = True
|
||||
del sys_modules['threading']
|
||||
del sys_modules["threading"]
|
||||
|
||||
if '_threading_local' in sys_modules:
|
||||
if "_threading_local" in sys_modules:
|
||||
# If ``_threading_local`` is already imported, might as well prevent
|
||||
# trying to import it more than needed by saving it if it is
|
||||
# already imported before deleting it.
|
||||
held__threading_local = sys_modules['_threading_local']
|
||||
held__threading_local = sys_modules["_threading_local"]
|
||||
holding__threading_local = True
|
||||
del sys_modules['_threading_local']
|
||||
del sys_modules["_threading_local"]
|
||||
|
||||
import threading
|
||||
|
||||
# Need a copy of the code kept somewhere...
|
||||
sys_modules['_dummy_threading'] = sys_modules['threading']
|
||||
del sys_modules['threading']
|
||||
sys_modules['_dummy__threading_local'] = sys_modules['_threading_local']
|
||||
del sys_modules['_threading_local']
|
||||
sys_modules["_dummy_threading"] = sys_modules["threading"]
|
||||
del sys_modules["threading"]
|
||||
# _threading_local may not be imported if _thread._local is available
|
||||
if "_threading_local" in sys_modules:
|
||||
sys_modules["_dummy__threading_local"] = sys_modules["_threading_local"]
|
||||
del sys_modules["_threading_local"]
|
||||
from _dummy_threading import *
|
||||
from _dummy_threading import __all__
|
||||
|
||||
@@ -55,23 +59,23 @@ finally:
|
||||
# Put back ``threading`` if we overwrote earlier
|
||||
|
||||
if holding_threading:
|
||||
sys_modules['threading'] = held_threading
|
||||
sys_modules["threading"] = held_threading
|
||||
del held_threading
|
||||
del holding_threading
|
||||
|
||||
# Put back ``_threading_local`` if we overwrote earlier
|
||||
|
||||
if holding__threading_local:
|
||||
sys_modules['_threading_local'] = held__threading_local
|
||||
sys_modules["_threading_local"] = held__threading_local
|
||||
del held__threading_local
|
||||
del holding__threading_local
|
||||
|
||||
# Put back ``thread`` if we overwrote, else del the entry we made
|
||||
if holding_thread:
|
||||
sys_modules['_thread'] = held_thread
|
||||
sys_modules["_thread"] = held_thread
|
||||
del held_thread
|
||||
else:
|
||||
del sys_modules['_thread']
|
||||
del sys_modules["_thread"]
|
||||
del holding_thread
|
||||
|
||||
del _dummy_thread
|
||||
|
||||
Vendored
+470
-210
@@ -2,6 +2,7 @@
|
||||
Various tests for synchronization primitives.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import sys
|
||||
import time
|
||||
from _thread import start_new_thread, TIMEOUT_MAX
|
||||
@@ -13,54 +14,79 @@ from test import support
|
||||
from test.support import threading_helper
|
||||
|
||||
|
||||
def _wait():
|
||||
# A crude wait/yield function not relying on synchronization primitives.
|
||||
time.sleep(0.01)
|
||||
requires_fork = unittest.skipUnless(support.has_fork_support,
|
||||
"platform doesn't support fork "
|
||||
"(no _at_fork_reinit method)")
|
||||
|
||||
|
||||
def wait_threads_blocked(nthread):
|
||||
# Arbitrary sleep to wait until N threads are blocked,
|
||||
# like waiting for a lock.
|
||||
time.sleep(0.010 * nthread)
|
||||
|
||||
|
||||
class Bunch(object):
|
||||
"""
|
||||
A bunch of threads.
|
||||
"""
|
||||
def __init__(self, f, n, wait_before_exit=False):
|
||||
def __init__(self, func, nthread, wait_before_exit=False):
|
||||
"""
|
||||
Construct a bunch of `n` threads running the same function `f`.
|
||||
Construct a bunch of `nthread` threads running the same function `func`.
|
||||
If `wait_before_exit` is True, the threads won't terminate until
|
||||
do_finish() is called.
|
||||
"""
|
||||
self.f = f
|
||||
self.n = n
|
||||
self.func = func
|
||||
self.nthread = nthread
|
||||
self.started = []
|
||||
self.finished = []
|
||||
self.exceptions = []
|
||||
self._can_exit = not wait_before_exit
|
||||
self.wait_thread = threading_helper.wait_threads_exit()
|
||||
self.wait_thread.__enter__()
|
||||
self._wait_thread = None
|
||||
|
||||
def task():
|
||||
tid = threading.get_ident()
|
||||
self.started.append(tid)
|
||||
try:
|
||||
f()
|
||||
finally:
|
||||
self.finished.append(tid)
|
||||
while not self._can_exit:
|
||||
_wait()
|
||||
def task(self):
|
||||
tid = threading.get_ident()
|
||||
self.started.append(tid)
|
||||
try:
|
||||
self.func()
|
||||
except BaseException as exc:
|
||||
self.exceptions.append(exc)
|
||||
finally:
|
||||
self.finished.append(tid)
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if self._can_exit:
|
||||
break
|
||||
|
||||
def __enter__(self):
|
||||
self._wait_thread = threading_helper.wait_threads_exit(support.SHORT_TIMEOUT)
|
||||
self._wait_thread.__enter__()
|
||||
|
||||
try:
|
||||
for i in range(n):
|
||||
start_new_thread(task, ())
|
||||
for _ in range(self.nthread):
|
||||
start_new_thread(self.task, ())
|
||||
except:
|
||||
self._can_exit = True
|
||||
raise
|
||||
|
||||
def wait_for_started(self):
|
||||
while len(self.started) < self.n:
|
||||
_wait()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(self.started) >= self.nthread:
|
||||
break
|
||||
|
||||
def wait_for_finished(self):
|
||||
while len(self.finished) < self.n:
|
||||
_wait()
|
||||
# Wait for threads exit
|
||||
self.wait_thread.__exit__(None, None, None)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(self.finished) >= self.nthread:
|
||||
break
|
||||
|
||||
# Wait until threads completely exit according to _thread._count()
|
||||
self._wait_thread.__exit__(None, None, None)
|
||||
|
||||
# Break reference cycle
|
||||
exceptions = self.exceptions
|
||||
self.exceptions = None
|
||||
if exceptions:
|
||||
raise ExceptionGroup(f"{self.func} threads raised exceptions",
|
||||
exceptions)
|
||||
|
||||
def do_finish(self):
|
||||
self._can_exit = True
|
||||
@@ -88,6 +114,12 @@ class BaseLockTests(BaseTestCase):
|
||||
Tests for both recursive and non-recursive locks.
|
||||
"""
|
||||
|
||||
def wait_phase(self, phase, expected):
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(phase) >= expected:
|
||||
break
|
||||
self.assertEqual(len(phase), expected)
|
||||
|
||||
def test_constructor(self):
|
||||
lock = self.locktype()
|
||||
del lock
|
||||
@@ -125,44 +157,60 @@ class BaseLockTests(BaseTestCase):
|
||||
result = []
|
||||
def f():
|
||||
result.append(lock.acquire(False))
|
||||
Bunch(f, 1).wait_for_finished()
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
self.assertFalse(result[0])
|
||||
lock.release()
|
||||
|
||||
@unittest.skip("TODO: RUSTPYTHON, sometimes hangs")
|
||||
@unittest.skip('TODO: RUSTPYTHON; sometimes hangs')
|
||||
def test_acquire_contended(self):
|
||||
lock = self.locktype()
|
||||
lock.acquire()
|
||||
N = 5
|
||||
def f():
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
|
||||
b = Bunch(f, N)
|
||||
b.wait_for_started()
|
||||
_wait()
|
||||
self.assertEqual(len(b.finished), 0)
|
||||
lock.release()
|
||||
b.wait_for_finished()
|
||||
self.assertEqual(len(b.finished), N)
|
||||
N = 5
|
||||
with Bunch(f, N) as bunch:
|
||||
# Threads block on lock.acquire()
|
||||
wait_threads_blocked(N)
|
||||
self.assertEqual(len(bunch.finished), 0)
|
||||
|
||||
# Threads unblocked
|
||||
lock.release()
|
||||
|
||||
self.assertEqual(len(bunch.finished), N)
|
||||
|
||||
def test_with(self):
|
||||
lock = self.locktype()
|
||||
def f():
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
def _with(err=None):
|
||||
|
||||
def with_lock(err=None):
|
||||
with lock:
|
||||
if err is not None:
|
||||
raise err
|
||||
_with()
|
||||
# Check the lock is unacquired
|
||||
Bunch(f, 1).wait_for_finished()
|
||||
self.assertRaises(TypeError, _with, TypeError)
|
||||
# Check the lock is unacquired
|
||||
Bunch(f, 1).wait_for_finished()
|
||||
|
||||
@unittest.skip("TODO: RUSTPYTHON, sometimes hangs")
|
||||
# Acquire the lock, do nothing, with releases the lock
|
||||
with lock:
|
||||
pass
|
||||
|
||||
# Check that the lock is unacquired
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
|
||||
# Acquire the lock, raise an exception, with releases the lock
|
||||
with self.assertRaises(TypeError):
|
||||
with lock:
|
||||
raise TypeError
|
||||
|
||||
# Check that the lock is unacquired even if after an exception
|
||||
# was raised in the previous "with lock:" block
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
|
||||
@unittest.skip('TODO: RUSTPYTHON; sometimes hangs')
|
||||
def test_thread_leak(self):
|
||||
# The lock shouldn't leak a Thread instance when used from a foreign
|
||||
# (non-threading) thread.
|
||||
@@ -170,22 +218,16 @@ class BaseLockTests(BaseTestCase):
|
||||
def f():
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
n = len(threading.enumerate())
|
||||
|
||||
# We run many threads in the hope that existing threads ids won't
|
||||
# be recycled.
|
||||
Bunch(f, 15).wait_for_finished()
|
||||
if len(threading.enumerate()) != n:
|
||||
# There is a small window during which a Thread instance's
|
||||
# target function has finished running, but the Thread is still
|
||||
# alive and registered. Avoid spurious failures by waiting a
|
||||
# bit more (seen on a buildbot).
|
||||
time.sleep(0.4)
|
||||
self.assertEqual(n, len(threading.enumerate()))
|
||||
with Bunch(f, 15):
|
||||
pass
|
||||
|
||||
def test_timeout(self):
|
||||
lock = self.locktype()
|
||||
# Can't set timeout if not blocking
|
||||
self.assertRaises(ValueError, lock.acquire, 0, 1)
|
||||
self.assertRaises(ValueError, lock.acquire, False, 1)
|
||||
# Invalid timeout values
|
||||
self.assertRaises(ValueError, lock.acquire, timeout=-100)
|
||||
self.assertRaises(OverflowError, lock.acquire, timeout=1e100)
|
||||
@@ -204,7 +246,8 @@ class BaseLockTests(BaseTestCase):
|
||||
results.append(lock.acquire(timeout=0.5))
|
||||
t2 = time.monotonic()
|
||||
results.append(t2 - t1)
|
||||
Bunch(f, 1).wait_for_finished()
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
self.assertFalse(results[0])
|
||||
self.assertTimeout(results[1], 0.5)
|
||||
|
||||
@@ -217,6 +260,7 @@ class BaseLockTests(BaseTestCase):
|
||||
lock = self.locktype()
|
||||
ref = weakref.ref(lock)
|
||||
del lock
|
||||
gc.collect() # For PyPy or other GCs.
|
||||
self.assertIsNone(ref())
|
||||
|
||||
|
||||
@@ -237,15 +281,13 @@ class LockTests(BaseLockTests):
|
||||
phase.append(None)
|
||||
|
||||
with threading_helper.wait_threads_exit():
|
||||
# Thread blocked on lock.acquire()
|
||||
start_new_thread(f, ())
|
||||
while len(phase) == 0:
|
||||
_wait()
|
||||
_wait()
|
||||
self.assertEqual(len(phase), 1)
|
||||
self.wait_phase(phase, 1)
|
||||
|
||||
# Thread unblocked
|
||||
lock.release()
|
||||
while len(phase) == 1:
|
||||
_wait()
|
||||
self.assertEqual(len(phase), 2)
|
||||
self.wait_phase(phase, 2)
|
||||
|
||||
def test_different_thread(self):
|
||||
# Lock can be released from a different thread.
|
||||
@@ -253,8 +295,8 @@ class LockTests(BaseLockTests):
|
||||
lock.acquire()
|
||||
def f():
|
||||
lock.release()
|
||||
b = Bunch(f, 1)
|
||||
b.wait_for_finished()
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
|
||||
@@ -268,6 +310,25 @@ class LockTests(BaseLockTests):
|
||||
self.assertFalse(lock.locked())
|
||||
self.assertTrue(lock.acquire(blocking=False))
|
||||
|
||||
@requires_fork
|
||||
def test_at_fork_reinit(self):
|
||||
def use_lock(lock):
|
||||
# make sure that the lock still works normally
|
||||
# after _at_fork_reinit()
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
|
||||
# unlocked
|
||||
lock = self.locktype()
|
||||
lock._at_fork_reinit()
|
||||
use_lock(lock)
|
||||
|
||||
# locked: _at_fork_reinit() resets the lock to the unlocked state
|
||||
lock2 = self.locktype()
|
||||
lock2.acquire()
|
||||
lock2._at_fork_reinit()
|
||||
use_lock(lock2)
|
||||
|
||||
|
||||
class RLockTests(BaseLockTests):
|
||||
"""
|
||||
@@ -306,17 +367,52 @@ class RLockTests(BaseLockTests):
|
||||
lock.release()
|
||||
self.assertRaises(RuntimeError, lock._release_save)
|
||||
|
||||
def test_recursion_count(self):
|
||||
lock = self.locktype()
|
||||
self.assertEqual(0, lock._recursion_count())
|
||||
lock.acquire()
|
||||
self.assertEqual(1, lock._recursion_count())
|
||||
lock.acquire()
|
||||
lock.acquire()
|
||||
self.assertEqual(3, lock._recursion_count())
|
||||
lock.release()
|
||||
self.assertEqual(2, lock._recursion_count())
|
||||
lock.release()
|
||||
lock.release()
|
||||
self.assertEqual(0, lock._recursion_count())
|
||||
|
||||
phase = []
|
||||
|
||||
def f():
|
||||
lock.acquire()
|
||||
phase.append(None)
|
||||
|
||||
self.wait_phase(phase, 2)
|
||||
lock.release()
|
||||
phase.append(None)
|
||||
|
||||
with threading_helper.wait_threads_exit():
|
||||
# Thread blocked on lock.acquire()
|
||||
start_new_thread(f, ())
|
||||
self.wait_phase(phase, 1)
|
||||
self.assertEqual(0, lock._recursion_count())
|
||||
|
||||
# Thread unblocked
|
||||
phase.append(None)
|
||||
self.wait_phase(phase, 3)
|
||||
self.assertEqual(0, lock._recursion_count())
|
||||
|
||||
def test_different_thread(self):
|
||||
# Cannot release from a different thread
|
||||
lock = self.locktype()
|
||||
def f():
|
||||
lock.acquire()
|
||||
b = Bunch(f, 1, True)
|
||||
try:
|
||||
self.assertRaises(RuntimeError, lock.release)
|
||||
finally:
|
||||
b.do_finish()
|
||||
b.wait_for_finished()
|
||||
|
||||
with Bunch(f, 1, True) as bunch:
|
||||
try:
|
||||
self.assertRaises(RuntimeError, lock.release)
|
||||
finally:
|
||||
bunch.do_finish()
|
||||
|
||||
def test__is_owned(self):
|
||||
lock = self.locktype()
|
||||
@@ -328,7 +424,8 @@ class RLockTests(BaseLockTests):
|
||||
result = []
|
||||
def f():
|
||||
result.append(lock._is_owned())
|
||||
Bunch(f, 1).wait_for_finished()
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
self.assertFalse(result[0])
|
||||
lock.release()
|
||||
self.assertTrue(lock._is_owned())
|
||||
@@ -361,12 +458,15 @@ class EventTests(BaseTestCase):
|
||||
def f():
|
||||
results1.append(evt.wait())
|
||||
results2.append(evt.wait())
|
||||
b = Bunch(f, N)
|
||||
b.wait_for_started()
|
||||
_wait()
|
||||
self.assertEqual(len(results1), 0)
|
||||
evt.set()
|
||||
b.wait_for_finished()
|
||||
|
||||
with Bunch(f, N):
|
||||
# Threads blocked on first evt.wait()
|
||||
wait_threads_blocked(N)
|
||||
self.assertEqual(len(results1), 0)
|
||||
|
||||
# Threads unblocked
|
||||
evt.set()
|
||||
|
||||
self.assertEqual(results1, [True] * N)
|
||||
self.assertEqual(results2, [True] * N)
|
||||
|
||||
@@ -389,46 +489,61 @@ class EventTests(BaseTestCase):
|
||||
r = evt.wait(0.5)
|
||||
t2 = time.monotonic()
|
||||
results2.append((r, t2 - t1))
|
||||
Bunch(f, N).wait_for_finished()
|
||||
|
||||
with Bunch(f, N):
|
||||
pass
|
||||
|
||||
self.assertEqual(results1, [False] * N)
|
||||
for r, dt in results2:
|
||||
self.assertFalse(r)
|
||||
self.assertTimeout(dt, 0.5)
|
||||
|
||||
# The event is set
|
||||
results1 = []
|
||||
results2 = []
|
||||
evt.set()
|
||||
Bunch(f, N).wait_for_finished()
|
||||
with Bunch(f, N):
|
||||
pass
|
||||
|
||||
self.assertEqual(results1, [True] * N)
|
||||
for r, dt in results2:
|
||||
self.assertTrue(r)
|
||||
|
||||
def test_set_and_clear(self):
|
||||
# Issue #13502: check that wait() returns true even when the event is
|
||||
# gh-57711: check that wait() returns true even when the event is
|
||||
# cleared before the waiting thread is woken up.
|
||||
evt = self.eventtype()
|
||||
event = self.eventtype()
|
||||
results = []
|
||||
timeout = 0.250
|
||||
N = 5
|
||||
def f():
|
||||
results.append(evt.wait(timeout * 4))
|
||||
b = Bunch(f, N)
|
||||
b.wait_for_started()
|
||||
time.sleep(timeout)
|
||||
evt.set()
|
||||
evt.clear()
|
||||
b.wait_for_finished()
|
||||
results.append(event.wait(support.LONG_TIMEOUT))
|
||||
|
||||
N = 5
|
||||
with Bunch(f, N):
|
||||
# Threads blocked on event.wait()
|
||||
wait_threads_blocked(N)
|
||||
|
||||
# Threads unblocked
|
||||
event.set()
|
||||
event.clear()
|
||||
|
||||
self.assertEqual(results, [True] * N)
|
||||
|
||||
def test_reset_internal_locks(self):
|
||||
@requires_fork
|
||||
def test_at_fork_reinit(self):
|
||||
# ensure that condition is still using a Lock after reset
|
||||
evt = self.eventtype()
|
||||
with evt._cond:
|
||||
self.assertFalse(evt._cond.acquire(False))
|
||||
evt._reset_internal_locks()
|
||||
evt._at_fork_reinit()
|
||||
with evt._cond:
|
||||
self.assertFalse(evt._cond.acquire(False))
|
||||
|
||||
def test_repr(self):
|
||||
evt = self.eventtype()
|
||||
self.assertRegex(repr(evt), r"<\w+\.Event at .*: unset>")
|
||||
evt.set()
|
||||
self.assertRegex(repr(evt), r"<\w+\.Event at .*: set>")
|
||||
|
||||
|
||||
class ConditionTests(BaseTestCase):
|
||||
"""
|
||||
@@ -466,15 +581,14 @@ class ConditionTests(BaseTestCase):
|
||||
# Note that this test is sensitive to timing. If the worker threads
|
||||
# don't execute in a timely fashion, the main thread may think they
|
||||
# are further along then they are. The main thread therefore issues
|
||||
# _wait() statements to try to make sure that it doesn't race ahead
|
||||
# of the workers.
|
||||
# wait_threads_blocked() statements to try to make sure that it doesn't
|
||||
# race ahead of the workers.
|
||||
# Secondly, this test assumes that condition variables are not subject
|
||||
# to spurious wakeups. The absence of spurious wakeups is an implementation
|
||||
# detail of Condition Variables in current CPython, but in general, not
|
||||
# a guaranteed property of condition variables as a programming
|
||||
# construct. In particular, it is possible that this can no longer
|
||||
# be conveniently guaranteed should their implementation ever change.
|
||||
N = 5
|
||||
ready = []
|
||||
results1 = []
|
||||
results2 = []
|
||||
@@ -483,58 +597,83 @@ class ConditionTests(BaseTestCase):
|
||||
cond.acquire()
|
||||
ready.append(phase_num)
|
||||
result = cond.wait()
|
||||
|
||||
cond.release()
|
||||
results1.append((result, phase_num))
|
||||
|
||||
cond.acquire()
|
||||
ready.append(phase_num)
|
||||
|
||||
result = cond.wait()
|
||||
cond.release()
|
||||
results2.append((result, phase_num))
|
||||
b = Bunch(f, N)
|
||||
b.wait_for_started()
|
||||
# first wait, to ensure all workers settle into cond.wait() before
|
||||
# we continue. See issues #8799 and #30727.
|
||||
while len(ready) < 5:
|
||||
_wait()
|
||||
ready.clear()
|
||||
self.assertEqual(results1, [])
|
||||
# Notify 3 threads at first
|
||||
cond.acquire()
|
||||
cond.notify(3)
|
||||
_wait()
|
||||
phase_num = 1
|
||||
cond.release()
|
||||
while len(results1) < 3:
|
||||
_wait()
|
||||
self.assertEqual(results1, [(True, 1)] * 3)
|
||||
self.assertEqual(results2, [])
|
||||
# make sure all awaken workers settle into cond.wait()
|
||||
while len(ready) < 3:
|
||||
_wait()
|
||||
# Notify 5 threads: they might be in their first or second wait
|
||||
cond.acquire()
|
||||
cond.notify(5)
|
||||
_wait()
|
||||
phase_num = 2
|
||||
cond.release()
|
||||
while len(results1) + len(results2) < 8:
|
||||
_wait()
|
||||
self.assertEqual(results1, [(True, 1)] * 3 + [(True, 2)] * 2)
|
||||
self.assertEqual(results2, [(True, 2)] * 3)
|
||||
# make sure all workers settle into cond.wait()
|
||||
while len(ready) < 5:
|
||||
_wait()
|
||||
# Notify all threads: they are all in their second wait
|
||||
cond.acquire()
|
||||
cond.notify_all()
|
||||
_wait()
|
||||
phase_num = 3
|
||||
cond.release()
|
||||
while len(results2) < 5:
|
||||
_wait()
|
||||
self.assertEqual(results1, [(True, 1)] * 3 + [(True,2)] * 2)
|
||||
self.assertEqual(results2, [(True, 2)] * 3 + [(True, 3)] * 2)
|
||||
b.wait_for_finished()
|
||||
|
||||
N = 5
|
||||
with Bunch(f, N):
|
||||
# first wait, to ensure all workers settle into cond.wait() before
|
||||
# we continue. See issues #8799 and #30727.
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(ready) >= N:
|
||||
break
|
||||
|
||||
ready.clear()
|
||||
self.assertEqual(results1, [])
|
||||
|
||||
# Notify 3 threads at first
|
||||
count1 = 3
|
||||
cond.acquire()
|
||||
cond.notify(count1)
|
||||
wait_threads_blocked(count1)
|
||||
|
||||
# Phase 1
|
||||
phase_num = 1
|
||||
cond.release()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(results1) >= count1:
|
||||
break
|
||||
|
||||
self.assertEqual(results1, [(True, 1)] * count1)
|
||||
self.assertEqual(results2, [])
|
||||
|
||||
# Wait until awaken workers are blocked on cond.wait()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(ready) >= count1 :
|
||||
break
|
||||
|
||||
# Notify 5 threads: they might be in their first or second wait
|
||||
cond.acquire()
|
||||
cond.notify(5)
|
||||
wait_threads_blocked(N)
|
||||
|
||||
# Phase 2
|
||||
phase_num = 2
|
||||
cond.release()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(results1) + len(results2) >= (N + count1):
|
||||
break
|
||||
|
||||
count2 = N - count1
|
||||
self.assertEqual(results1, [(True, 1)] * count1 + [(True, 2)] * count2)
|
||||
self.assertEqual(results2, [(True, 2)] * count1)
|
||||
|
||||
# Make sure all workers settle into cond.wait()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(ready) >= N:
|
||||
break
|
||||
|
||||
# Notify all threads: they are all in their second wait
|
||||
cond.acquire()
|
||||
cond.notify_all()
|
||||
wait_threads_blocked(N)
|
||||
|
||||
# Phase 3
|
||||
phase_num = 3
|
||||
cond.release()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(results2) >= N:
|
||||
break
|
||||
self.assertEqual(results1, [(True, 1)] * count1 + [(True, 2)] * count2)
|
||||
self.assertEqual(results2, [(True, 2)] * count1 + [(True, 3)] * count2)
|
||||
|
||||
def test_notify(self):
|
||||
cond = self.condtype()
|
||||
@@ -544,19 +683,23 @@ class ConditionTests(BaseTestCase):
|
||||
|
||||
def test_timeout(self):
|
||||
cond = self.condtype()
|
||||
timeout = 0.5
|
||||
results = []
|
||||
N = 5
|
||||
def f():
|
||||
cond.acquire()
|
||||
t1 = time.monotonic()
|
||||
result = cond.wait(0.5)
|
||||
result = cond.wait(timeout)
|
||||
t2 = time.monotonic()
|
||||
cond.release()
|
||||
results.append((t2 - t1, result))
|
||||
Bunch(f, N).wait_for_finished()
|
||||
|
||||
N = 5
|
||||
with Bunch(f, N):
|
||||
pass
|
||||
self.assertEqual(len(results), N)
|
||||
|
||||
for dt, result in results:
|
||||
self.assertTimeout(dt, 0.5)
|
||||
self.assertTimeout(dt, timeout)
|
||||
# Note that conceptually (that"s the condition variable protocol)
|
||||
# a wait() may succeed even if no one notifies us and before any
|
||||
# timeout occurs. Spurious wakeups can occur.
|
||||
@@ -569,17 +712,16 @@ class ConditionTests(BaseTestCase):
|
||||
state = 0
|
||||
def f():
|
||||
with cond:
|
||||
result = cond.wait_for(lambda : state==4)
|
||||
result = cond.wait_for(lambda: state == 4)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(state, 4)
|
||||
b = Bunch(f, 1)
|
||||
b.wait_for_started()
|
||||
for i in range(4):
|
||||
time.sleep(0.01)
|
||||
with cond:
|
||||
state += 1
|
||||
cond.notify()
|
||||
b.wait_for_finished()
|
||||
|
||||
with Bunch(f, 1):
|
||||
for i in range(4):
|
||||
time.sleep(0.010)
|
||||
with cond:
|
||||
state += 1
|
||||
cond.notify()
|
||||
|
||||
def test_waitfor_timeout(self):
|
||||
cond = self.condtype()
|
||||
@@ -593,15 +735,15 @@ class ConditionTests(BaseTestCase):
|
||||
self.assertFalse(result)
|
||||
self.assertTimeout(dt, 0.1)
|
||||
success.append(None)
|
||||
b = Bunch(f, 1)
|
||||
b.wait_for_started()
|
||||
# Only increment 3 times, so state == 4 is never reached.
|
||||
for i in range(3):
|
||||
time.sleep(0.01)
|
||||
with cond:
|
||||
state += 1
|
||||
cond.notify()
|
||||
b.wait_for_finished()
|
||||
|
||||
with Bunch(f, 1):
|
||||
# Only increment 3 times, so state == 4 is never reached.
|
||||
for i in range(3):
|
||||
time.sleep(0.010)
|
||||
with cond:
|
||||
state += 1
|
||||
cond.notify()
|
||||
|
||||
self.assertEqual(len(success), 1)
|
||||
|
||||
|
||||
@@ -630,41 +772,107 @@ class BaseSemaphoreTests(BaseTestCase):
|
||||
del sem
|
||||
|
||||
def test_acquire_contended(self):
|
||||
sem = self.semtype(7)
|
||||
sem_value = 7
|
||||
sem = self.semtype(sem_value)
|
||||
sem.acquire()
|
||||
N = 10
|
||||
|
||||
sem_results = []
|
||||
results1 = []
|
||||
results2 = []
|
||||
phase_num = 0
|
||||
def f():
|
||||
|
||||
def func():
|
||||
sem_results.append(sem.acquire())
|
||||
results1.append(phase_num)
|
||||
|
||||
sem_results.append(sem.acquire())
|
||||
results2.append(phase_num)
|
||||
b = Bunch(f, 10)
|
||||
b.wait_for_started()
|
||||
while len(results1) + len(results2) < 6:
|
||||
_wait()
|
||||
self.assertEqual(results1 + results2, [0] * 6)
|
||||
phase_num = 1
|
||||
for i in range(7):
|
||||
|
||||
def wait_count(count):
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(results1) + len(results2) >= count:
|
||||
break
|
||||
|
||||
N = 10
|
||||
with Bunch(func, N):
|
||||
# Phase 0
|
||||
count1 = sem_value - 1
|
||||
wait_count(count1)
|
||||
self.assertEqual(results1 + results2, [0] * count1)
|
||||
|
||||
# Phase 1
|
||||
phase_num = 1
|
||||
for i in range(sem_value):
|
||||
sem.release()
|
||||
count2 = sem_value
|
||||
wait_count(count1 + count2)
|
||||
self.assertEqual(sorted(results1 + results2),
|
||||
[0] * count1 + [1] * count2)
|
||||
|
||||
# Phase 2
|
||||
phase_num = 2
|
||||
count3 = (sem_value - 1)
|
||||
for i in range(count3):
|
||||
sem.release()
|
||||
wait_count(count1 + count2 + count3)
|
||||
self.assertEqual(sorted(results1 + results2),
|
||||
[0] * count1 + [1] * count2 + [2] * count3)
|
||||
# The semaphore is still locked
|
||||
self.assertFalse(sem.acquire(False))
|
||||
|
||||
# Final release, to let the last thread finish
|
||||
count4 = 1
|
||||
sem.release()
|
||||
while len(results1) + len(results2) < 13:
|
||||
_wait()
|
||||
self.assertEqual(sorted(results1 + results2), [0] * 6 + [1] * 7)
|
||||
phase_num = 2
|
||||
for i in range(6):
|
||||
|
||||
self.assertEqual(sem_results,
|
||||
[True] * (count1 + count2 + count3 + count4))
|
||||
|
||||
def test_multirelease(self):
|
||||
sem_value = 7
|
||||
sem = self.semtype(sem_value)
|
||||
sem.acquire()
|
||||
|
||||
results1 = []
|
||||
results2 = []
|
||||
phase_num = 0
|
||||
def func():
|
||||
sem.acquire()
|
||||
results1.append(phase_num)
|
||||
|
||||
sem.acquire()
|
||||
results2.append(phase_num)
|
||||
|
||||
def wait_count(count):
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if len(results1) + len(results2) >= count:
|
||||
break
|
||||
|
||||
with Bunch(func, 10):
|
||||
# Phase 0
|
||||
count1 = sem_value - 1
|
||||
wait_count(count1)
|
||||
self.assertEqual(results1 + results2, [0] * count1)
|
||||
|
||||
# Phase 1
|
||||
phase_num = 1
|
||||
count2 = sem_value
|
||||
sem.release(count2)
|
||||
wait_count(count1 + count2)
|
||||
self.assertEqual(sorted(results1 + results2),
|
||||
[0] * count1 + [1] * count2)
|
||||
|
||||
# Phase 2
|
||||
phase_num = 2
|
||||
count3 = sem_value - 1
|
||||
sem.release(count3)
|
||||
wait_count(count1 + count2 + count3)
|
||||
self.assertEqual(sorted(results1 + results2),
|
||||
[0] * count1 + [1] * count2 + [2] * count3)
|
||||
# The semaphore is still locked
|
||||
self.assertFalse(sem.acquire(False))
|
||||
|
||||
# Final release, to let the last thread finish
|
||||
sem.release()
|
||||
while len(results1) + len(results2) < 19:
|
||||
_wait()
|
||||
self.assertEqual(sorted(results1 + results2), [0] * 6 + [1] * 7 + [2] * 6)
|
||||
# The semaphore is still locked
|
||||
self.assertFalse(sem.acquire(False))
|
||||
# Final release, to let the last thread finish
|
||||
sem.release()
|
||||
b.wait_for_finished()
|
||||
self.assertEqual(sem_results, [True] * (6 + 7 + 6 + 1))
|
||||
|
||||
def test_try_acquire(self):
|
||||
sem = self.semtype(2)
|
||||
@@ -681,7 +889,8 @@ class BaseSemaphoreTests(BaseTestCase):
|
||||
def f():
|
||||
results.append(sem.acquire(False))
|
||||
results.append(sem.acquire(False))
|
||||
Bunch(f, 5).wait_for_finished()
|
||||
with Bunch(f, 5):
|
||||
pass
|
||||
# There can be a thread switch between acquiring the semaphore and
|
||||
# appending the result, therefore results will not necessarily be
|
||||
# ordered.
|
||||
@@ -707,12 +916,14 @@ class BaseSemaphoreTests(BaseTestCase):
|
||||
def f():
|
||||
sem.acquire()
|
||||
sem.release()
|
||||
b = Bunch(f, 1)
|
||||
b.wait_for_started()
|
||||
_wait()
|
||||
self.assertFalse(b.finished)
|
||||
sem.release()
|
||||
b.wait_for_finished()
|
||||
|
||||
with Bunch(f, 1) as bunch:
|
||||
# Thread blocked on sem.acquire()
|
||||
wait_threads_blocked(1)
|
||||
self.assertFalse(bunch.finished)
|
||||
|
||||
# Thread unblocked
|
||||
sem.release()
|
||||
|
||||
def test_with(self):
|
||||
sem = self.semtype(2)
|
||||
@@ -744,6 +955,15 @@ class SemaphoreTests(BaseSemaphoreTests):
|
||||
sem.acquire()
|
||||
sem.release()
|
||||
|
||||
def test_repr(self):
|
||||
sem = self.semtype(3)
|
||||
self.assertRegex(repr(sem), r"<\w+\.Semaphore at .*: value=3>")
|
||||
sem.acquire()
|
||||
self.assertRegex(repr(sem), r"<\w+\.Semaphore at .*: value=2>")
|
||||
sem.release()
|
||||
sem.release()
|
||||
self.assertRegex(repr(sem), r"<\w+\.Semaphore at .*: value=4>")
|
||||
|
||||
|
||||
class BoundedSemaphoreTests(BaseSemaphoreTests):
|
||||
"""
|
||||
@@ -758,6 +978,12 @@ class BoundedSemaphoreTests(BaseSemaphoreTests):
|
||||
sem.release()
|
||||
self.assertRaises(ValueError, sem.release)
|
||||
|
||||
def test_repr(self):
|
||||
sem = self.semtype(3)
|
||||
self.assertRegex(repr(sem), r"<\w+\.BoundedSemaphore at .*: value=3/3>")
|
||||
sem.acquire()
|
||||
self.assertRegex(repr(sem), r"<\w+\.BoundedSemaphore at .*: value=2/3>")
|
||||
|
||||
|
||||
class BarrierTests(BaseTestCase):
|
||||
"""
|
||||
@@ -768,13 +994,13 @@ class BarrierTests(BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.barrier = self.barriertype(self.N, timeout=self.defaultTimeout)
|
||||
|
||||
def tearDown(self):
|
||||
self.barrier.abort()
|
||||
|
||||
def run_threads(self, f):
|
||||
b = Bunch(f, self.N-1)
|
||||
f()
|
||||
b.wait_for_finished()
|
||||
with Bunch(f, self.N):
|
||||
pass
|
||||
|
||||
def multipass(self, results, n):
|
||||
m = self.barrier.parties
|
||||
@@ -789,6 +1015,10 @@ class BarrierTests(BaseTestCase):
|
||||
self.assertEqual(self.barrier.n_waiting, 0)
|
||||
self.assertFalse(self.barrier.broken)
|
||||
|
||||
def test_constructor(self):
|
||||
self.assertRaises(ValueError, self.barriertype, parties=0)
|
||||
self.assertRaises(ValueError, self.barriertype, parties=-1)
|
||||
|
||||
def test_barrier(self, passes=1):
|
||||
"""
|
||||
Test that a barrier is passed in lockstep
|
||||
@@ -865,8 +1095,9 @@ class BarrierTests(BaseTestCase):
|
||||
i = self.barrier.wait()
|
||||
if i == self.N//2:
|
||||
# Wait until the other threads are all in the barrier.
|
||||
while self.barrier.n_waiting < self.N-1:
|
||||
time.sleep(0.001)
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if self.barrier.n_waiting >= (self.N - 1):
|
||||
break
|
||||
self.barrier.reset()
|
||||
else:
|
||||
try:
|
||||
@@ -926,27 +1157,56 @@ class BarrierTests(BaseTestCase):
|
||||
i = self.barrier.wait()
|
||||
if i == self.N // 2:
|
||||
# One thread is late!
|
||||
time.sleep(1.0)
|
||||
time.sleep(self.defaultTimeout / 2)
|
||||
# Default timeout is 2.0, so this is shorter.
|
||||
self.assertRaises(threading.BrokenBarrierError,
|
||||
self.barrier.wait, 0.5)
|
||||
self.barrier.wait, self.defaultTimeout / 4)
|
||||
self.run_threads(f)
|
||||
|
||||
def test_default_timeout(self):
|
||||
"""
|
||||
Test the barrier's default timeout
|
||||
"""
|
||||
# create a barrier with a low default timeout
|
||||
barrier = self.barriertype(self.N, timeout=0.3)
|
||||
timeout = 0.100
|
||||
barrier = self.barriertype(2, timeout=timeout)
|
||||
def f():
|
||||
i = barrier.wait()
|
||||
if i == self.N // 2:
|
||||
# One thread is later than the default timeout of 0.3s.
|
||||
time.sleep(1.0)
|
||||
self.assertRaises(threading.BrokenBarrierError, barrier.wait)
|
||||
self.run_threads(f)
|
||||
self.assertRaises(threading.BrokenBarrierError,
|
||||
barrier.wait)
|
||||
|
||||
start_time = time.monotonic()
|
||||
with Bunch(f, 1):
|
||||
pass
|
||||
dt = time.monotonic() - start_time
|
||||
self.assertGreaterEqual(dt, timeout)
|
||||
|
||||
def test_single_thread(self):
|
||||
b = self.barriertype(1)
|
||||
b.wait()
|
||||
b.wait()
|
||||
|
||||
def test_repr(self):
|
||||
barrier = self.barriertype(3)
|
||||
timeout = support.LONG_TIMEOUT
|
||||
self.assertRegex(repr(barrier), r"<\w+\.Barrier at .*: waiters=0/3>")
|
||||
def f():
|
||||
barrier.wait(timeout)
|
||||
|
||||
N = 2
|
||||
with Bunch(f, N):
|
||||
# Threads blocked on barrier.wait()
|
||||
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
|
||||
if barrier.n_waiting >= N:
|
||||
break
|
||||
self.assertRegex(repr(barrier),
|
||||
r"<\w+\.Barrier at .*: waiters=2/3>")
|
||||
|
||||
# Threads unblocked
|
||||
barrier.wait(timeout)
|
||||
|
||||
self.assertRegex(repr(barrier),
|
||||
r"<\w+\.Barrier at .*: waiters=0/3>")
|
||||
|
||||
# Abort the barrier
|
||||
barrier.abort()
|
||||
self.assertRegex(repr(barrier),
|
||||
r"<\w+\.Barrier at .*: broken>")
|
||||
@@ -190,7 +190,6 @@ class ProcessPoolExecutorTest(ExecutorTest):
|
||||
for i, future in enumerate(futures):
|
||||
self.assertEqual(future.result(), mul(i, i))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON AttributeError: module 'threading' has no attribute '_start_joinable_thread'. Did you mean: '_start_new_thread'?
|
||||
def test_python_finalization_error(self):
|
||||
# gh-109047: Catch RuntimeError on thread creation
|
||||
# during Python finalization.
|
||||
|
||||
+5
-1
@@ -108,7 +108,6 @@ class ExecutorShutdownTest:
|
||||
# one finished.
|
||||
self.assertGreater(len(others), 0)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON AssertionError: b'' != b'apple'
|
||||
def test_hang_gh83386(self):
|
||||
"""shutdown(wait=False) doesn't hang at exit with running futures.
|
||||
|
||||
@@ -256,6 +255,11 @@ class ThreadPoolShutdownTest(ThreadPoolMixin, ExecutorShutdownTest, BaseTestCase
|
||||
|
||||
|
||||
class ProcessPoolShutdownTest(ExecutorShutdownTest):
|
||||
# TODO: RUSTPYTHON - flaky, dict changed size during iteration race condition
|
||||
@unittest.skip("TODO: RUSTPYTHON - flaky race condition on macOS")
|
||||
def test_cancel_futures(self):
|
||||
return super().test_cancel_futures()
|
||||
|
||||
def test_processes_terminate(self):
|
||||
def acquire_lock(lock):
|
||||
lock.acquire()
|
||||
|
||||
@@ -66,14 +66,8 @@ class ThreadPoolExecutorTest(ThreadPoolMixin, ExecutorTest, BaseTestCase):
|
||||
with futures.ProcessPoolExecutor(1, mp_context=mp.get_context('fork')) as workers:
|
||||
workers.submit(tuple)
|
||||
|
||||
import sys # TODO: RUSTPYTHON
|
||||
@unittest.skipIf(
|
||||
'RUSTPYTHON_SKIP_ENV_POLLUTERS' in os.environ,
|
||||
'TODO: RUSTPYTHON environment pollution when running rustpython -m test --fail-env-changed due to unknown reason'
|
||||
)
|
||||
@support.requires_fork()
|
||||
@unittest.skipUnless(hasattr(os, 'register_at_fork'), 'need os.register_at_fork')
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON AssertionError: DeprecationWarning not triggered
|
||||
def test_process_fork_from_a_threadpool(self):
|
||||
# bpo-43944: clear concurrent.futures.thread._threads_queues after fork,
|
||||
# otherwise child process will try to join parent thread
|
||||
|
||||
+4
-1
@@ -29,6 +29,8 @@ class ModuleLockAsRLockTests:
|
||||
test_timeout = None
|
||||
# _release_save() unsupported
|
||||
test_release_save_unacquired = None
|
||||
# _recursion_count() unsupported
|
||||
test_recursion_count = None
|
||||
# lock status in repr unsupported
|
||||
test_repr = None
|
||||
test_locked_repr = None
|
||||
@@ -92,7 +94,8 @@ class DeadlockAvoidanceTests:
|
||||
b.release()
|
||||
if ra:
|
||||
a.release()
|
||||
lock_tests.Bunch(f, NTHREADS).wait_for_finished()
|
||||
with lock_tests.Bunch(f, NTHREADS):
|
||||
pass
|
||||
self.assertEqual(len(results), NTHREADS)
|
||||
return results
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -920,7 +920,7 @@ class SysModuleTest(unittest.TestCase):
|
||||
def test_clear_type_cache(self):
|
||||
sys._clear_type_cache()
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
@unittest.skip("TODO: RUSTPYTHON; cp424 encoding not supported, causes panic")
|
||||
@force_not_colorized
|
||||
@support.requires_subprocess()
|
||||
def test_ioencoding(self):
|
||||
|
||||
Vendored
+687
-240
File diff suppressed because it is too large.
Load diff
Vendored
+171
-225
@@ -3,11 +3,11 @@
|
||||
import os as _os
|
||||
import sys as _sys
|
||||
import _thread
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
from time import monotonic as _time
|
||||
from _weakrefset import WeakSet
|
||||
from itertools import islice as _islice, count as _count
|
||||
from itertools import count as _count
|
||||
try:
|
||||
from _collections import deque as _deque
|
||||
except ImportError:
|
||||
@@ -28,13 +28,20 @@ __all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
|
||||
'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
|
||||
'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
|
||||
'setprofile', 'settrace', 'local', 'stack_size',
|
||||
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile']
|
||||
'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile',
|
||||
'setprofile_all_threads','settrace_all_threads']
|
||||
|
||||
# Rename some stuff so "from threading import *" is safe
|
||||
_start_new_thread = _thread.start_new_thread
|
||||
_start_joinable_thread = _thread.start_joinable_thread
|
||||
_daemon_threads_allowed = _thread.daemon_threads_allowed
|
||||
_allocate_lock = _thread.allocate_lock
|
||||
_set_sentinel = _thread._set_sentinel
|
||||
_LockType = _thread.LockType
|
||||
_thread_shutdown = _thread._shutdown
|
||||
_make_thread_handle = _thread._make_thread_handle
|
||||
_ThreadHandle = _thread._ThreadHandle
|
||||
get_ident = _thread.get_ident
|
||||
_get_main_thread_ident = _thread._get_main_thread_ident
|
||||
_is_main_interpreter = _thread._is_main_interpreter
|
||||
try:
|
||||
get_native_id = _thread.get_native_id
|
||||
_HAVE_THREAD_NATIVE_ID = True
|
||||
@@ -49,6 +56,13 @@ except AttributeError:
|
||||
TIMEOUT_MAX = _thread.TIMEOUT_MAX
|
||||
del _thread
|
||||
|
||||
# get thread-local implementation, either from the thread
|
||||
# module, or from the python fallback
|
||||
|
||||
try:
|
||||
from _thread import _local as local
|
||||
except ImportError:
|
||||
from _threading_local import local
|
||||
|
||||
# Support for profile and trace hooks
|
||||
|
||||
@@ -60,11 +74,20 @@ def setprofile(func):
|
||||
|
||||
The func will be passed to sys.setprofile() for each thread, before its
|
||||
run() method is called.
|
||||
|
||||
"""
|
||||
global _profile_hook
|
||||
_profile_hook = func
|
||||
|
||||
def setprofile_all_threads(func):
|
||||
"""Set a profile function for all threads started from the threading module
|
||||
and all Python threads that are currently executing.
|
||||
|
||||
The func will be passed to sys.setprofile() for each thread, before its
|
||||
run() method is called.
|
||||
"""
|
||||
setprofile(func)
|
||||
_sys._setprofileallthreads(func)
|
||||
|
||||
def getprofile():
|
||||
"""Get the profiler function as set by threading.setprofile()."""
|
||||
return _profile_hook
|
||||
@@ -74,12 +97,10 @@ def settrace(func):
|
||||
|
||||
The func will be passed to sys.settrace() for each thread, before its run()
|
||||
method is called.
|
||||
|
||||
"""
|
||||
global _trace_hook
|
||||
_trace_hook = func
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
def settrace_all_threads(func):
|
||||
"""Set a trace function for all threads started from the threading module
|
||||
and all Python threads that are currently executing.
|
||||
@@ -96,7 +117,7 @@ def gettrace():
|
||||
|
||||
# Synchronization classes
|
||||
|
||||
Lock = _allocate_lock
|
||||
Lock = _LockType
|
||||
|
||||
def RLock(*args, **kwargs):
|
||||
"""Factory function that returns a new reentrant lock.
|
||||
@@ -107,6 +128,12 @@ def RLock(*args, **kwargs):
|
||||
acquired it.
|
||||
|
||||
"""
|
||||
if args or kwargs:
|
||||
warnings.warn(
|
||||
'Passing arguments to RLock is deprecated and will be removed in 3.15',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if _CRLock is None:
|
||||
return _PyRLock(*args, **kwargs)
|
||||
return _CRLock(*args, **kwargs)
|
||||
@@ -229,6 +256,13 @@ class _RLock:
|
||||
def _is_owned(self):
|
||||
return self._owner == get_ident()
|
||||
|
||||
# Internal method used for reentrancy checks
|
||||
|
||||
def _recursion_count(self):
|
||||
if self._owner != get_ident():
|
||||
return 0
|
||||
return self._count
|
||||
|
||||
_PyRLock = _RLock
|
||||
|
||||
|
||||
@@ -254,18 +288,12 @@ class Condition:
|
||||
# If the lock defines _release_save() and/or _acquire_restore(),
|
||||
# these override the default implementations (which just call
|
||||
# release() and acquire() on the lock). Ditto for _is_owned().
|
||||
try:
|
||||
if hasattr(lock, '_release_save'):
|
||||
self._release_save = lock._release_save
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
if hasattr(lock, '_acquire_restore'):
|
||||
self._acquire_restore = lock._acquire_restore
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
if hasattr(lock, '_is_owned'):
|
||||
self._is_owned = lock._is_owned
|
||||
except AttributeError:
|
||||
pass
|
||||
self._waiters = _deque()
|
||||
|
||||
def _at_fork_reinit(self):
|
||||
@@ -308,7 +336,7 @@ class Condition:
|
||||
awakened or timed out, it re-acquires the lock and returns.
|
||||
|
||||
When the timeout argument is present and not None, it should be a
|
||||
floating point number specifying a timeout for the operation in seconds
|
||||
floating-point number specifying a timeout for the operation in seconds
|
||||
(or fractions thereof).
|
||||
|
||||
When the underlying lock is an RLock, it is not released using its
|
||||
@@ -436,6 +464,11 @@ class Semaphore:
|
||||
self._cond = Condition(Lock())
|
||||
self._value = value
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:"
|
||||
f" value={self._value}>")
|
||||
|
||||
def acquire(self, blocking=True, timeout=None):
|
||||
"""Acquire a semaphore, decrementing the internal counter by one.
|
||||
|
||||
@@ -494,8 +527,7 @@ class Semaphore:
|
||||
raise ValueError('n must be one or more')
|
||||
with self._cond:
|
||||
self._value += n
|
||||
for i in range(n):
|
||||
self._cond.notify()
|
||||
self._cond.notify(n)
|
||||
|
||||
def __exit__(self, t, v, tb):
|
||||
self.release()
|
||||
@@ -519,9 +551,14 @@ class BoundedSemaphore(Semaphore):
|
||||
"""
|
||||
|
||||
def __init__(self, value=1):
|
||||
Semaphore.__init__(self, value)
|
||||
super().__init__(value)
|
||||
self._initial_value = value
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:"
|
||||
f" value={self._value}/{self._initial_value}>")
|
||||
|
||||
def release(self, n=1):
|
||||
"""Release a semaphore, incrementing the internal counter by one or more.
|
||||
|
||||
@@ -538,8 +575,7 @@ class BoundedSemaphore(Semaphore):
|
||||
if self._value + n > self._initial_value:
|
||||
raise ValueError("Semaphore released too many times")
|
||||
self._value += n
|
||||
for i in range(n):
|
||||
self._cond.notify()
|
||||
self._cond.notify(n)
|
||||
|
||||
|
||||
class Event:
|
||||
@@ -557,8 +593,13 @@ class Event:
|
||||
self._cond = Condition(Lock())
|
||||
self._flag = False
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
status = 'set' if self._flag else 'unset'
|
||||
return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: {status}>"
|
||||
|
||||
def _at_fork_reinit(self):
|
||||
# Private method called by Thread._reset_internal_locks()
|
||||
# Private method called by Thread._after_fork()
|
||||
self._cond._at_fork_reinit()
|
||||
|
||||
def is_set(self):
|
||||
@@ -568,7 +609,7 @@ class Event:
|
||||
def isSet(self):
|
||||
"""Return true if and only if the internal flag is true.
|
||||
|
||||
This method is deprecated, use notify_all() instead.
|
||||
This method is deprecated, use is_set() instead.
|
||||
|
||||
"""
|
||||
import warnings
|
||||
@@ -605,11 +646,12 @@ class Event:
|
||||
the optional timeout occurs.
|
||||
|
||||
When the timeout argument is present and not None, it should be a
|
||||
floating point number specifying a timeout for the operation in seconds
|
||||
floating-point number specifying a timeout for the operation in seconds
|
||||
(or fractions thereof).
|
||||
|
||||
This method returns the internal flag on exit, so it will always return
|
||||
True except if a timeout is given and the operation times out.
|
||||
``True`` except if a timeout is given and the operation times out, when
|
||||
it will return ``False``.
|
||||
|
||||
"""
|
||||
with self._cond:
|
||||
@@ -648,6 +690,8 @@ class Barrier:
|
||||
default for all subsequent 'wait()' calls.
|
||||
|
||||
"""
|
||||
if parties < 1:
|
||||
raise ValueError("parties must be >= 1")
|
||||
self._cond = Condition(Lock())
|
||||
self._action = action
|
||||
self._timeout = timeout
|
||||
@@ -655,6 +699,13 @@ class Barrier:
|
||||
self._state = 0 # 0 filling, 1 draining, -1 resetting, -2 broken
|
||||
self._count = 0
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
if self.broken:
|
||||
return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: broken>"
|
||||
return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:"
|
||||
f" waiters={self.n_waiting}/{self.parties}>")
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""Wait for the barrier.
|
||||
|
||||
@@ -802,25 +853,6 @@ _active = {} # maps thread id to Thread object
|
||||
_limbo = {}
|
||||
_dangling = WeakSet()
|
||||
|
||||
# Set of Thread._tstate_lock locks of non-daemon threads used by _shutdown()
|
||||
# to wait until all Python thread states get deleted:
|
||||
# see Thread._set_tstate_lock().
|
||||
_shutdown_locks_lock = _allocate_lock()
|
||||
_shutdown_locks = set()
|
||||
|
||||
def _maintain_shutdown_locks():
|
||||
"""
|
||||
Drop any shutdown locks that don't correspond to running threads anymore.
|
||||
|
||||
Calling this from time to time avoids an ever-growing _shutdown_locks
|
||||
set when Thread objects are not joined explicitly. See bpo-37788.
|
||||
|
||||
This must be called with _shutdown_locks_lock acquired.
|
||||
"""
|
||||
# If a lock was released, the corresponding thread has exited
|
||||
to_remove = [lock for lock in _shutdown_locks if not lock.locked()]
|
||||
_shutdown_locks.difference_update(to_remove)
|
||||
|
||||
|
||||
# Main class for threads
|
||||
|
||||
@@ -848,7 +880,7 @@ class Thread:
|
||||
*name* is the thread name. By default, a unique name is constructed of
|
||||
the form "Thread-N" where N is a small decimal number.
|
||||
|
||||
*args* is the argument tuple for the target invocation. Defaults to ().
|
||||
*args* is a list or tuple of arguments for the target invocation. Defaults to ().
|
||||
|
||||
*kwargs* is a dictionary of keyword arguments for the target
|
||||
invocation. Defaults to {}.
|
||||
@@ -877,15 +909,16 @@ class Thread:
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
if daemon is not None:
|
||||
if daemon and not _daemon_threads_allowed():
|
||||
raise RuntimeError('daemon threads are disabled in this (sub)interpreter')
|
||||
self._daemonic = daemon
|
||||
else:
|
||||
self._daemonic = current_thread().daemon
|
||||
self._ident = None
|
||||
if _HAVE_THREAD_NATIVE_ID:
|
||||
self._native_id = None
|
||||
self._tstate_lock = None
|
||||
self._handle = _ThreadHandle()
|
||||
self._started = Event()
|
||||
self._is_stopped = False
|
||||
self._initialized = True
|
||||
# Copy of sys.stderr used by self._invoke_excepthook()
|
||||
self._stderr = _sys.stderr
|
||||
@@ -893,30 +926,26 @@ class Thread:
|
||||
# For debugging and _after_fork()
|
||||
_dangling.add(self)
|
||||
|
||||
def _reset_internal_locks(self, is_alive):
|
||||
# private! Called by _after_fork() to reset our internal locks as
|
||||
# they may be in an invalid state leading to a deadlock or crash.
|
||||
def _after_fork(self, new_ident=None):
|
||||
# Private! Called by threading._after_fork().
|
||||
self._started._at_fork_reinit()
|
||||
if is_alive:
|
||||
# bpo-42350: If the fork happens when the thread is already stopped
|
||||
# (ex: after threading._shutdown() has been called), _tstate_lock
|
||||
# is None. Do nothing in this case.
|
||||
if self._tstate_lock is not None:
|
||||
self._tstate_lock._at_fork_reinit()
|
||||
self._tstate_lock.acquire()
|
||||
if new_ident is not None:
|
||||
# This thread is alive.
|
||||
self._ident = new_ident
|
||||
assert self._handle.ident == new_ident
|
||||
if _HAVE_THREAD_NATIVE_ID:
|
||||
self._set_native_id()
|
||||
else:
|
||||
# The thread isn't alive after fork: it doesn't have a tstate
|
||||
# anymore.
|
||||
self._is_stopped = True
|
||||
self._tstate_lock = None
|
||||
# Otherwise, the thread is dead, Jim. _PyThread_AfterFork()
|
||||
# already marked our handle done.
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
assert self._initialized, "Thread.__init__() was not called"
|
||||
status = "initial"
|
||||
if self._started.is_set():
|
||||
status = "started"
|
||||
self.is_alive() # easy way to get ._is_stopped set when appropriate
|
||||
if self._is_stopped:
|
||||
if self._handle.is_done():
|
||||
status = "stopped"
|
||||
if self._daemonic:
|
||||
status += " daemon"
|
||||
@@ -943,12 +972,14 @@ class Thread:
|
||||
with _active_limbo_lock:
|
||||
_limbo[self] = self
|
||||
try:
|
||||
_start_new_thread(self._bootstrap, ())
|
||||
# Start joinable thread
|
||||
_start_joinable_thread(self._bootstrap, handle=self._handle,
|
||||
daemon=self.daemon)
|
||||
except Exception:
|
||||
with _active_limbo_lock:
|
||||
del _limbo[self]
|
||||
raise
|
||||
self._started.wait()
|
||||
self._started.wait() # Will set ident and native_id
|
||||
|
||||
def run(self):
|
||||
"""Method representing the thread's activity.
|
||||
@@ -994,23 +1025,9 @@ class Thread:
|
||||
def _set_native_id(self):
|
||||
self._native_id = get_native_id()
|
||||
|
||||
def _set_tstate_lock(self):
|
||||
"""
|
||||
Set a lock object which will be released by the interpreter when
|
||||
the underlying thread state (see pystate.h) gets deleted.
|
||||
"""
|
||||
self._tstate_lock = _set_sentinel()
|
||||
self._tstate_lock.acquire()
|
||||
|
||||
if not self.daemon:
|
||||
with _shutdown_locks_lock:
|
||||
_maintain_shutdown_locks()
|
||||
_shutdown_locks.add(self._tstate_lock)
|
||||
|
||||
def _bootstrap_inner(self):
|
||||
try:
|
||||
self._set_ident()
|
||||
self._set_tstate_lock()
|
||||
if _HAVE_THREAD_NATIVE_ID:
|
||||
self._set_native_id()
|
||||
self._started.set()
|
||||
@@ -1028,40 +1045,7 @@ class Thread:
|
||||
except:
|
||||
self._invoke_excepthook(self)
|
||||
finally:
|
||||
with _active_limbo_lock:
|
||||
try:
|
||||
# We don't call self._delete() because it also
|
||||
# grabs _active_limbo_lock.
|
||||
del _active[get_ident()]
|
||||
except:
|
||||
pass
|
||||
|
||||
def _stop(self):
|
||||
# After calling ._stop(), .is_alive() returns False and .join() returns
|
||||
# immediately. ._tstate_lock must be released before calling ._stop().
|
||||
#
|
||||
# Normal case: C code at the end of the thread's life
|
||||
# (release_sentinel in _threadmodule.c) releases ._tstate_lock, and
|
||||
# that's detected by our ._wait_for_tstate_lock(), called by .join()
|
||||
# and .is_alive(). Any number of threads _may_ call ._stop()
|
||||
# simultaneously (for example, if multiple threads are blocked in
|
||||
# .join() calls), and they're not serialized. That's harmless -
|
||||
# they'll just make redundant rebindings of ._is_stopped and
|
||||
# ._tstate_lock. Obscure: we rebind ._tstate_lock last so that the
|
||||
# "assert self._is_stopped" in ._wait_for_tstate_lock() always works
|
||||
# (the assert is executed only if ._tstate_lock is None).
|
||||
#
|
||||
# Special case: _main_thread releases ._tstate_lock via this
|
||||
# module's _shutdown() function.
|
||||
lock = self._tstate_lock
|
||||
if lock is not None:
|
||||
assert not lock.locked()
|
||||
self._is_stopped = True
|
||||
self._tstate_lock = None
|
||||
if not self.daemon:
|
||||
with _shutdown_locks_lock:
|
||||
# Remove our lock and other released locks from _shutdown_locks
|
||||
_maintain_shutdown_locks()
|
||||
self._delete()
|
||||
|
||||
def _delete(self):
|
||||
"Remove current thread from the dict of currently running threads."
|
||||
@@ -1080,7 +1064,7 @@ class Thread:
|
||||
or until the optional timeout occurs.
|
||||
|
||||
When the timeout argument is present and not None, it should be a
|
||||
floating point number specifying a timeout for the operation in seconds
|
||||
floating-point number specifying a timeout for the operation in seconds
|
||||
(or fractions thereof). As join() always returns None, you must call
|
||||
is_alive() after join() to decide whether a timeout happened -- if the
|
||||
thread is still alive, the join() call timed out.
|
||||
@@ -1103,39 +1087,12 @@ class Thread:
|
||||
if self is current_thread():
|
||||
raise RuntimeError("cannot join current thread")
|
||||
|
||||
if timeout is None:
|
||||
self._wait_for_tstate_lock()
|
||||
else:
|
||||
# the behavior of a negative timeout isn't documented, but
|
||||
# historically .join(timeout=x) for x<0 has acted as if timeout=0
|
||||
self._wait_for_tstate_lock(timeout=max(timeout, 0))
|
||||
# the behavior of a negative timeout isn't documented, but
|
||||
# historically .join(timeout=x) for x<0 has acted as if timeout=0
|
||||
if timeout is not None:
|
||||
timeout = max(timeout, 0)
|
||||
|
||||
def _wait_for_tstate_lock(self, block=True, timeout=-1):
|
||||
# Issue #18808: wait for the thread state to be gone.
|
||||
# At the end of the thread's life, after all knowledge of the thread
|
||||
# is removed from C data structures, C code releases our _tstate_lock.
|
||||
# This method passes its arguments to _tstate_lock.acquire().
|
||||
# If the lock is acquired, the C code is done, and self._stop() is
|
||||
# called. That sets ._is_stopped to True, and ._tstate_lock to None.
|
||||
lock = self._tstate_lock
|
||||
if lock is None:
|
||||
# already determined that the C code is done
|
||||
assert self._is_stopped
|
||||
return
|
||||
|
||||
try:
|
||||
if lock.acquire(block, timeout):
|
||||
lock.release()
|
||||
self._stop()
|
||||
except:
|
||||
if lock.locked():
|
||||
# bpo-45274: lock.acquire() acquired the lock, but the function
|
||||
# was interrupted with an exception before reaching the
|
||||
# lock.release(). It can happen if a signal handler raises an
|
||||
# exception, like CTRL+C which raises KeyboardInterrupt.
|
||||
lock.release()
|
||||
self._stop()
|
||||
raise
|
||||
self._handle.join(timeout)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -1186,10 +1143,7 @@ class Thread:
|
||||
|
||||
"""
|
||||
assert self._initialized, "Thread.__init__() not called"
|
||||
if self._is_stopped or not self._started.is_set():
|
||||
return False
|
||||
self._wait_for_tstate_lock(False)
|
||||
return not self._is_stopped
|
||||
return self._started.is_set() and not self._handle.is_done()
|
||||
|
||||
@property
|
||||
def daemon(self):
|
||||
@@ -1210,6 +1164,8 @@ class Thread:
|
||||
def daemon(self, daemonic):
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Thread.__init__() not called")
|
||||
if daemonic and not _daemon_threads_allowed():
|
||||
raise RuntimeError('daemon threads are disabled in this interpreter')
|
||||
if self._started.is_set():
|
||||
raise RuntimeError("cannot set daemon status of active thread")
|
||||
self._daemonic = daemonic
|
||||
@@ -1396,19 +1352,45 @@ class _MainThread(Thread):
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self, name="MainThread", daemon=False)
|
||||
self._set_tstate_lock()
|
||||
self._started.set()
|
||||
self._set_ident()
|
||||
self._ident = _get_main_thread_ident()
|
||||
self._handle = _make_thread_handle(self._ident)
|
||||
if _HAVE_THREAD_NATIVE_ID:
|
||||
self._set_native_id()
|
||||
with _active_limbo_lock:
|
||||
_active[self._ident] = self
|
||||
|
||||
|
||||
# Helper thread-local instance to detect when a _DummyThread
|
||||
# is collected. Not a part of the public API.
|
||||
_thread_local_info = local()
|
||||
|
||||
|
||||
class _DeleteDummyThreadOnDel:
|
||||
'''
|
||||
Helper class to remove a dummy thread from threading._active on __del__.
|
||||
'''
|
||||
|
||||
def __init__(self, dummy_thread):
|
||||
self._dummy_thread = dummy_thread
|
||||
self._tident = dummy_thread.ident
|
||||
# Put the thread on a thread local variable so that when
|
||||
# the related thread finishes this instance is collected.
|
||||
#
|
||||
# Note: no other references to this instance may be created.
|
||||
# If any client code creates a reference to this instance,
|
||||
# the related _DummyThread will be kept forever!
|
||||
_thread_local_info._track_dummy_thread_ref = self
|
||||
|
||||
def __del__(self, _active_limbo_lock=_active_limbo_lock, _active=_active):
|
||||
with _active_limbo_lock:
|
||||
if _active.get(self._tident) is self._dummy_thread:
|
||||
_active.pop(self._tident, None)
|
||||
|
||||
|
||||
# Dummy thread class to represent threads not started here.
|
||||
# These aren't garbage collected when they die, nor can they be waited for.
|
||||
# If they invoke anything in threading.py that calls current_thread(), they
|
||||
# leave an entry in the _active dict forever after.
|
||||
# These should be added to `_active` and removed automatically
|
||||
# when they die, although they can't be waited for.
|
||||
# Their purpose is to return *something* from current_thread().
|
||||
# They are marked as daemon threads so we won't wait for them
|
||||
# when we exit (conform previous semantics).
|
||||
@@ -1416,24 +1398,31 @@ class _MainThread(Thread):
|
||||
class _DummyThread(Thread):
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
|
||||
|
||||
Thread.__init__(self, name=_newname("Dummy-%d"),
|
||||
daemon=_daemon_threads_allowed())
|
||||
self._started.set()
|
||||
self._set_ident()
|
||||
self._handle = _make_thread_handle(self._ident)
|
||||
if _HAVE_THREAD_NATIVE_ID:
|
||||
self._set_native_id()
|
||||
with _active_limbo_lock:
|
||||
_active[self._ident] = self
|
||||
|
||||
def _stop(self):
|
||||
pass
|
||||
_DeleteDummyThreadOnDel(self)
|
||||
|
||||
def is_alive(self):
|
||||
assert not self._is_stopped and self._started.is_set()
|
||||
return True
|
||||
if not self._handle.is_done() and self._started.is_set():
|
||||
return True
|
||||
raise RuntimeError("thread is not alive")
|
||||
|
||||
def join(self, timeout=None):
|
||||
assert False, "cannot join a dummy thread"
|
||||
raise RuntimeError("cannot join a dummy thread")
|
||||
|
||||
def _after_fork(self, new_ident=None):
|
||||
if new_ident is not None:
|
||||
self.__class__ = _MainThread
|
||||
self._name = 'MainThread'
|
||||
self._daemonic = False
|
||||
Thread._after_fork(self, new_ident=new_ident)
|
||||
|
||||
|
||||
# Global API functions
|
||||
@@ -1468,6 +1457,8 @@ def active_count():
|
||||
enumerate().
|
||||
|
||||
"""
|
||||
# NOTE: if the logic in here ever changes, update Modules/posixmodule.c
|
||||
# warn_about_fork_with_threads() to match.
|
||||
with _active_limbo_lock:
|
||||
return len(_active) + len(_limbo)
|
||||
|
||||
@@ -1514,8 +1505,7 @@ def _register_atexit(func, *arg, **kwargs):
|
||||
if _SHUTTING_DOWN:
|
||||
raise RuntimeError("can't register atexit after shutdown")
|
||||
|
||||
call = functools.partial(func, *arg, **kwargs)
|
||||
_threading_atexits.append(call)
|
||||
_threading_atexits.append(lambda: func(*arg, **kwargs))
|
||||
|
||||
|
||||
from _thread import stack_size
|
||||
@@ -1530,12 +1520,11 @@ def _shutdown():
|
||||
"""
|
||||
Wait until the Python thread state of all non-daemon threads get deleted.
|
||||
"""
|
||||
# Obscure: other threads may be waiting to join _main_thread. That's
|
||||
# dubious, but some code does it. We can't wait for C code to release
|
||||
# the main thread's tstate_lock - that won't happen until the interpreter
|
||||
# is nearly dead. So we release it here. Note that just calling _stop()
|
||||
# isn't enough: other threads may already be waiting on _tstate_lock.
|
||||
if _main_thread._is_stopped:
|
||||
# Obscure: other threads may be waiting to join _main_thread. That's
|
||||
# dubious, but some code does it. We can't wait for it to be marked as done
|
||||
# normally - that won't happen until the interpreter is nearly dead. So
|
||||
# mark it done here.
|
||||
if _main_thread._handle.is_done() and _is_main_interpreter():
|
||||
# _shutdown() was already called
|
||||
return
|
||||
|
||||
@@ -1547,39 +1536,11 @@ def _shutdown():
|
||||
for atexit_call in reversed(_threading_atexits):
|
||||
atexit_call()
|
||||
|
||||
# Main thread
|
||||
if _main_thread.ident == get_ident():
|
||||
tlock = _main_thread._tstate_lock
|
||||
# The main thread isn't finished yet, so its thread state lock can't
|
||||
# have been released.
|
||||
assert tlock is not None
|
||||
assert tlock.locked()
|
||||
tlock.release()
|
||||
_main_thread._stop()
|
||||
else:
|
||||
# bpo-1596321: _shutdown() must be called in the main thread.
|
||||
# If the threading module was not imported by the main thread,
|
||||
# _main_thread is the thread which imported the threading module.
|
||||
# In this case, ignore _main_thread, similar behavior than for threads
|
||||
# spawned by C libraries or using _thread.start_new_thread().
|
||||
pass
|
||||
if _is_main_interpreter():
|
||||
_main_thread._handle._set_done()
|
||||
|
||||
# Join all non-deamon threads
|
||||
while True:
|
||||
with _shutdown_locks_lock:
|
||||
locks = list(_shutdown_locks)
|
||||
_shutdown_locks.clear()
|
||||
|
||||
if not locks:
|
||||
break
|
||||
|
||||
for lock in locks:
|
||||
# mimic Thread.join()
|
||||
lock.acquire()
|
||||
lock.release()
|
||||
|
||||
# new threads can be spawned while we were waiting for the other
|
||||
# threads to complete
|
||||
# Wait for all non-daemon threads to exit.
|
||||
_thread_shutdown()
|
||||
|
||||
|
||||
def main_thread():
|
||||
@@ -1588,16 +1549,9 @@ def main_thread():
|
||||
In normal conditions, the main thread is the thread from which the
|
||||
Python interpreter was started.
|
||||
"""
|
||||
# XXX Figure this out for subinterpreters. (See gh-75698.)
|
||||
return _main_thread
|
||||
|
||||
# get thread-local implementation, either from the thread
|
||||
# module, or from the python fallback
|
||||
|
||||
try:
|
||||
from _thread import _local as local
|
||||
except ImportError:
|
||||
from _threading_local import local
|
||||
|
||||
|
||||
def _after_fork():
|
||||
"""
|
||||
@@ -1606,7 +1560,6 @@ def _after_fork():
|
||||
# Reset _active_limbo_lock, in case we forked while the lock was held
|
||||
# by another (non-forked) thread. http://bugs.python.org/issue874900
|
||||
global _active_limbo_lock, _main_thread
|
||||
global _shutdown_locks_lock, _shutdown_locks
|
||||
_active_limbo_lock = RLock()
|
||||
|
||||
# fork() only copied the current thread; clear references to others.
|
||||
@@ -1622,10 +1575,6 @@ def _after_fork():
|
||||
|
||||
_main_thread = current
|
||||
|
||||
# reset _shutdown() locks: threads re-register their _tstate_lock below
|
||||
_shutdown_locks_lock = _allocate_lock()
|
||||
_shutdown_locks = set()
|
||||
|
||||
with _active_limbo_lock:
|
||||
# Dangling thread instances must still have their locks reset,
|
||||
# because someone may join() them.
|
||||
@@ -1635,16 +1584,13 @@ def _after_fork():
|
||||
# Any lock/condition variable may be currently locked or in an
|
||||
# invalid state, so we reinitialize them.
|
||||
if thread is current:
|
||||
# There is only one active thread. We reset the ident to
|
||||
# its new value since it can have changed.
|
||||
thread._reset_internal_locks(True)
|
||||
# This is the one and only active thread.
|
||||
ident = get_ident()
|
||||
thread._ident = ident
|
||||
thread._after_fork(new_ident=ident)
|
||||
new_active[ident] = thread
|
||||
else:
|
||||
# All the others are already stopped.
|
||||
thread._reset_internal_locks(False)
|
||||
thread._stop()
|
||||
thread._after_fork()
|
||||
|
||||
_limbo.clear()
|
||||
_active.clear()
|
||||
|
||||
@@ -57,14 +57,14 @@ impl PyAsyncGen {
|
||||
|
||||
zelf.ag_hooks_inited.store(true);
|
||||
|
||||
// Get and store finalizer from thread-local storage
|
||||
let finalizer = crate::vm::thread::ASYNC_GEN_FINALIZER.with_borrow(|f| f.as_ref().cloned());
|
||||
// Get and store finalizer from VM
|
||||
let finalizer = vm.async_gen_finalizer.borrow().clone();
|
||||
if let Some(finalizer) = finalizer {
|
||||
*zelf.ag_finalizer.lock() = Some(finalizer);
|
||||
}
|
||||
|
||||
// Call firstiter hook
|
||||
let firstiter = crate::vm::thread::ASYNC_GEN_FIRSTITER.with_borrow(|f| f.as_ref().cloned());
|
||||
let firstiter = vm.async_gen_firstiter.borrow().clone();
|
||||
if let Some(firstiter) = firstiter {
|
||||
let obj: PyObjectRef = zelf.to_owned().into();
|
||||
firstiter.call((obj,), vm)?;
|
||||
|
||||
@@ -661,6 +661,11 @@ pub mod module {
|
||||
}
|
||||
|
||||
fn py_os_after_fork_child(vm: &VirtualMachine) {
|
||||
// Mark all other threads as done before running Python callbacks
|
||||
// See _PyThread_AfterFork behavior
|
||||
#[cfg(feature = "threading")]
|
||||
crate::stdlib::thread::after_fork_child(vm);
|
||||
|
||||
let after_forkers_child: Vec<PyObjectRef> = vm.state.after_forkers_child.lock().clone();
|
||||
run_at_forkers(after_forkers_child, false, vm);
|
||||
}
|
||||
@@ -670,8 +675,64 @@ pub mod module {
|
||||
run_at_forkers(after_forkers_parent, false, vm);
|
||||
}
|
||||
|
||||
/// Warn if forking from a multi-threaded process
|
||||
fn warn_if_multi_threaded(name: &str, vm: &VirtualMachine) {
|
||||
// Only check threading if it was already imported
|
||||
// Avoid vm.import() which can execute arbitrary Python code in the fork path
|
||||
let threading = match vm
|
||||
.sys_module
|
||||
.get_attr("modules", vm)
|
||||
.and_then(|m| m.get_item("threading", vm))
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(_) => return,
|
||||
};
|
||||
let active = threading.get_attr("_active", vm).ok();
|
||||
let limbo = threading.get_attr("_limbo", vm).ok();
|
||||
|
||||
let count_dict = |obj: Option<crate::PyObjectRef>| -> usize {
|
||||
obj.and_then(|o| o.length_opt(vm))
|
||||
.and_then(|r| r.ok())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let num_threads = count_dict(active) + count_dict(limbo);
|
||||
if num_threads > 1 {
|
||||
// Use Python warnings module to ensure filters are applied correctly
|
||||
let Ok(warnings) = vm.import("warnings", 0) else {
|
||||
return;
|
||||
};
|
||||
let Ok(warn_fn) = warnings.get_attr("warn", vm) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pid = unsafe { libc::getpid() };
|
||||
let msg = format!(
|
||||
"This process (pid={}) is multi-threaded, use of {}() may lead to deadlocks in the child.",
|
||||
pid, name
|
||||
);
|
||||
|
||||
// Call warnings.warn(message, DeprecationWarning, stacklevel=2)
|
||||
// stacklevel=2 to point to the caller of fork()
|
||||
let args = crate::function::FuncArgs::new(
|
||||
vec![
|
||||
vm.ctx.new_str(msg).into(),
|
||||
vm.ctx.exceptions.deprecation_warning.as_object().to_owned(),
|
||||
],
|
||||
crate::function::KwArgs::new(
|
||||
[("stacklevel".to_owned(), vm.ctx.new_int(2).into())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
let _ = warn_fn.call(args, vm);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn fork(vm: &VirtualMachine) -> i32 {
|
||||
warn_if_multi_threaded("fork", vm);
|
||||
|
||||
let pid: i32;
|
||||
py_os_before_fork(vm);
|
||||
unsafe {
|
||||
|
||||
@@ -815,6 +815,32 @@ mod sys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a dictionary mapping each thread's identifier to the topmost stack frame
|
||||
/// currently active in that thread at the time the function is called.
|
||||
#[cfg(feature = "threading")]
|
||||
#[pyfunction]
|
||||
fn _current_frames(vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
||||
use crate::AsObject;
|
||||
use crate::stdlib::thread::get_all_current_frames;
|
||||
|
||||
let frames = get_all_current_frames(vm);
|
||||
let dict = vm.ctx.new_dict();
|
||||
|
||||
for (thread_id, frame) in frames {
|
||||
let key = vm.ctx.new_int(thread_id);
|
||||
dict.set_item(key.as_object(), frame.into(), vm)?;
|
||||
}
|
||||
|
||||
Ok(dict)
|
||||
}
|
||||
|
||||
/// Stub for non-threading builds - returns empty dict
|
||||
#[cfg(not(feature = "threading"))]
|
||||
#[pyfunction]
|
||||
fn _current_frames(vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
||||
Ok(vm.ctx.new_dict())
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn gettrace(vm: &VirtualMachine) -> PyObjectRef {
|
||||
vm.trace_func.borrow().clone()
|
||||
@@ -1107,6 +1133,22 @@ mod sys {
|
||||
update_use_tracing(vm);
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _settraceallthreads(tracefunc: PyObjectRef, vm: &VirtualMachine) {
|
||||
let func = (!vm.is_none(&tracefunc)).then(|| tracefunc.clone());
|
||||
*vm.state.global_trace_func.lock() = func;
|
||||
vm.trace_func.replace(tracefunc);
|
||||
update_use_tracing(vm);
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _setprofileallthreads(profilefunc: PyObjectRef, vm: &VirtualMachine) {
|
||||
let func = (!vm.is_none(&profilefunc)).then(|| profilefunc.clone());
|
||||
*vm.state.global_profile_func.lock() = func;
|
||||
vm.profile_func.replace(profilefunc);
|
||||
update_use_tracing(vm);
|
||||
}
|
||||
|
||||
#[cfg(feature = "threading")]
|
||||
#[pyattr]
|
||||
fn thread_info(vm: &VirtualMachine) -> PyTupleRef {
|
||||
@@ -1187,10 +1229,10 @@ mod sys {
|
||||
}
|
||||
|
||||
if let Some(finalizer) = args.finalizer.into_option() {
|
||||
crate::vm::thread::ASYNC_GEN_FINALIZER.set(finalizer);
|
||||
*vm.async_gen_finalizer.borrow_mut() = finalizer;
|
||||
}
|
||||
if let Some(firstiter) = args.firstiter.into_option() {
|
||||
crate::vm::thread::ASYNC_GEN_FIRSTITER.set(firstiter);
|
||||
*vm.async_gen_firstiter.borrow_mut() = firstiter;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1212,12 +1254,8 @@ mod sys {
|
||||
#[pyfunction]
|
||||
fn get_asyncgen_hooks(vm: &VirtualMachine) -> AsyncgenHooksData {
|
||||
AsyncgenHooksData {
|
||||
firstiter: crate::vm::thread::ASYNC_GEN_FIRSTITER
|
||||
.with_borrow(Clone::clone)
|
||||
.to_pyobject(vm),
|
||||
finalizer: crate::vm::thread::ASYNC_GEN_FINALIZER
|
||||
.with_borrow(Clone::clone)
|
||||
.to_pyobject(vm),
|
||||
firstiter: vm.async_gen_firstiter.borrow().clone().to_pyobject(vm),
|
||||
finalizer: vm.async_gen_finalizer.borrow().clone().to_pyobject(vm),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+590
-23
@@ -1,13 +1,16 @@
|
||||
//! Implementation of the _thread module
|
||||
#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
|
||||
pub(crate) use _thread::{RawRMutex, make_module};
|
||||
pub(crate) use _thread::{
|
||||
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, after_fork_child,
|
||||
get_all_current_frames, get_ident, init_main_thread_ident, make_module,
|
||||
};
|
||||
|
||||
#[pymodule]
|
||||
pub(crate) mod _thread {
|
||||
use crate::{
|
||||
AsObject, Py, PyPayload, PyRef, PyResult, VirtualMachine,
|
||||
builtins::{PyDictRef, PyStr, PyTupleRef, PyType, PyTypeRef},
|
||||
convert::ToPyException,
|
||||
frame::FrameRef,
|
||||
function::{ArgCallable, Either, FuncArgs, KwArgs, OptionalArg, PySetterValue},
|
||||
types::{Constructor, GetAttr, Representable, SetAttr},
|
||||
};
|
||||
@@ -19,7 +22,6 @@ pub(crate) mod _thread {
|
||||
lock_api::{RawMutex as RawMutexT, RawMutexTimed, RawReentrantMutex},
|
||||
};
|
||||
use std::thread;
|
||||
use thread_local::ThreadLocal;
|
||||
|
||||
// PYTHREAD_NAME: show current thread name
|
||||
pub const PYTHREAD_NAME: Option<&str> = {
|
||||
@@ -110,7 +112,8 @@ pub(crate) mod _thread {
|
||||
}
|
||||
|
||||
#[pyattr(name = "LockType")]
|
||||
#[pyclass(module = "thread", name = "lock")]
|
||||
#[pyattr(name = "lock")]
|
||||
#[pyclass(module = "_thread", name = "lock")]
|
||||
#[derive(PyPayload)]
|
||||
struct Lock {
|
||||
mu: RawMutex,
|
||||
@@ -172,10 +175,10 @@ pub(crate) mod _thread {
|
||||
}
|
||||
|
||||
impl Constructor for Lock {
|
||||
type Args = FuncArgs;
|
||||
type Args = ();
|
||||
|
||||
fn py_new(_cls: &Py<PyType>, _args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
|
||||
Err(vm.new_type_error("cannot create '_thread.lock' instances"))
|
||||
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
|
||||
Ok(Self { mu: RawMutex::INIT })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +191,7 @@ pub(crate) mod _thread {
|
||||
|
||||
pub type RawRMutex = RawReentrantMutex<RawMutex, RawThreadId>;
|
||||
#[pyattr]
|
||||
#[pyclass(module = "thread", name = "RLock")]
|
||||
#[pyclass(module = "_thread", name = "RLock")]
|
||||
#[derive(PyPayload)]
|
||||
struct RLock {
|
||||
mu: RawRMutex,
|
||||
@@ -201,7 +204,7 @@ pub(crate) mod _thread {
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(with(Representable))]
|
||||
#[pyclass(with(Representable), flags(BASETYPE))]
|
||||
impl RLock {
|
||||
#[pyslot]
|
||||
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
@@ -249,9 +252,10 @@ pub(crate) mod _thread {
|
||||
}
|
||||
self.count.store(0, core::sync::atomic::Ordering::Relaxed);
|
||||
let new_mut = RawRMutex::INIT;
|
||||
|
||||
let old_mutex: AtomicCell<&RawRMutex> = AtomicCell::new(&self.mu);
|
||||
old_mutex.swap(&new_mut);
|
||||
unsafe {
|
||||
let old_mutex: &AtomicCell<RawRMutex> = core::mem::transmute(&self.mu);
|
||||
old_mutex.swap(new_mut);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -283,12 +287,28 @@ pub(crate) mod _thread {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get thread identity - uses pthread_self() on Unix for fork compatibility
|
||||
#[pyfunction]
|
||||
fn get_ident() -> u64 {
|
||||
thread_to_id(&thread::current())
|
||||
pub fn get_ident() -> u64 {
|
||||
current_thread_id()
|
||||
}
|
||||
|
||||
fn thread_to_id(t: &thread::Thread) -> u64 {
|
||||
/// Get OS-level thread ID (pthread_self on Unix)
|
||||
/// This is important for fork compatibility - the ID must remain stable after fork
|
||||
#[cfg(unix)]
|
||||
fn current_thread_id() -> u64 {
|
||||
// pthread_self() like CPython for fork compatibility
|
||||
unsafe { libc::pthread_self() as u64 }
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn current_thread_id() -> u64 {
|
||||
thread_to_rust_id(&thread::current())
|
||||
}
|
||||
|
||||
/// Convert Rust thread to ID (used for non-unix platforms)
|
||||
#[cfg(not(unix))]
|
||||
fn thread_to_rust_id(t: &thread::Thread) -> u64 {
|
||||
use core::hash::{Hash, Hasher};
|
||||
struct U64Hash {
|
||||
v: Option<u64>,
|
||||
@@ -304,13 +324,25 @@ pub(crate) mod _thread {
|
||||
self.v.expect("should have written a u64")
|
||||
}
|
||||
}
|
||||
// TODO: use id.as_u64() once it's stable, until then, ThreadId is just a wrapper
|
||||
// around NonZeroU64, so this should work (?)
|
||||
let mut h = U64Hash { v: None };
|
||||
t.id().hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Get thread ID for a given thread handle (used by start_new_thread)
|
||||
fn thread_to_id(handle: &thread::JoinHandle<()>) -> u64 {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// On Unix, use pthread ID from the handle
|
||||
use std::os::unix::thread::JoinHandleExt;
|
||||
handle.as_pthread_t() as u64
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
thread_to_rust_id(handle.thread())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
const fn allocate_lock() -> Lock {
|
||||
Lock { mu: RawMutex::INIT }
|
||||
@@ -343,9 +375,9 @@ pub(crate) mod _thread {
|
||||
)
|
||||
.map(|handle| {
|
||||
vm.state.thread_count.fetch_add(1);
|
||||
thread_to_id(handle.thread())
|
||||
thread_to_id(&handle)
|
||||
})
|
||||
.map_err(|err| err.to_pyexception(vm))
|
||||
.map_err(|err| vm.new_runtime_error(format!("can't start new thread: {err}")))
|
||||
}
|
||||
|
||||
fn run_thread(func: ArgCallable, args: FuncArgs, vm: &VirtualMachine) {
|
||||
@@ -365,9 +397,24 @@ pub(crate) mod _thread {
|
||||
unsafe { lock.mu.unlock() };
|
||||
}
|
||||
}
|
||||
// Clean up thread-local storage while VM context is still active
|
||||
// This ensures __del__ methods are called properly
|
||||
cleanup_thread_local_data();
|
||||
// Clean up frame tracking
|
||||
crate::vm::thread::cleanup_current_thread_frames(vm);
|
||||
vm.state.thread_count.fetch_sub(1);
|
||||
}
|
||||
|
||||
/// Clean up thread-local data for the current thread.
|
||||
/// This triggers __del__ on objects stored in thread-local variables.
|
||||
fn cleanup_thread_local_data() {
|
||||
// Take all guards - this will trigger LocalGuard::drop for each,
|
||||
// which removes the thread's dict from each Local instance
|
||||
LOCAL_GUARDS.with(|guards| {
|
||||
guards.borrow_mut().clear();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[pyfunction]
|
||||
fn interrupt_main(signum: OptionalArg<i32>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
@@ -400,6 +447,107 @@ pub(crate) mod _thread {
|
||||
vm.state.thread_count.load()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn daemon_threads_allowed() -> bool {
|
||||
// RustPython always allows daemon threads
|
||||
true
|
||||
}
|
||||
|
||||
// Registry for non-daemon threads that need to be joined at shutdown
|
||||
pub type ShutdownEntry = (
|
||||
std::sync::Weak<parking_lot::Mutex<ThreadHandleInner>>,
|
||||
std::sync::Weak<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
||||
);
|
||||
|
||||
#[pyfunction]
|
||||
fn _shutdown(vm: &VirtualMachine) {
|
||||
// Wait for all non-daemon threads to finish
|
||||
let current_ident = get_ident();
|
||||
|
||||
loop {
|
||||
// Find a thread that's not finished and not the current thread
|
||||
let handle_to_join = {
|
||||
let mut handles = vm.state.shutdown_handles.lock();
|
||||
// Clean up finished entries
|
||||
handles.retain(|(inner_weak, _): &ShutdownEntry| {
|
||||
inner_weak.upgrade().map_or(false, |inner| {
|
||||
let guard = inner.lock();
|
||||
guard.state != ThreadHandleState::Done && guard.ident != current_ident
|
||||
})
|
||||
});
|
||||
|
||||
// Find first unfinished handle
|
||||
handles
|
||||
.iter()
|
||||
.find_map(|(inner_weak, done_event_weak): &ShutdownEntry| {
|
||||
let inner = inner_weak.upgrade()?;
|
||||
let done_event = done_event_weak.upgrade()?;
|
||||
let guard = inner.lock();
|
||||
if guard.state != ThreadHandleState::Done && guard.ident != current_ident {
|
||||
Some((inner.clone(), done_event.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
match handle_to_join {
|
||||
Some((_, done_event)) => {
|
||||
// Wait for this thread to finish (infinite timeout)
|
||||
// Only check done flag to avoid lock ordering issues
|
||||
// (done_event lock vs inner lock)
|
||||
let (lock, cvar) = &*done_event;
|
||||
let mut done = lock.lock();
|
||||
while !*done {
|
||||
cvar.wait(&mut done);
|
||||
}
|
||||
}
|
||||
None => break, // No more threads to wait on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a non-daemon thread handle to the shutdown registry
|
||||
fn add_to_shutdown_handles(
|
||||
vm: &VirtualMachine,
|
||||
inner: &std::sync::Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
||||
done_event: &std::sync::Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
||||
) {
|
||||
let mut handles = vm.state.shutdown_handles.lock();
|
||||
handles.push((
|
||||
std::sync::Arc::downgrade(inner),
|
||||
std::sync::Arc::downgrade(done_event),
|
||||
));
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _make_thread_handle(ident: u64, vm: &VirtualMachine) -> PyRef<ThreadHandle> {
|
||||
let handle = ThreadHandle::new(vm);
|
||||
{
|
||||
let mut inner = handle.inner.lock();
|
||||
inner.ident = ident;
|
||||
inner.state = ThreadHandleState::Running;
|
||||
}
|
||||
handle.into_ref(&vm.ctx)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _get_main_thread_ident(vm: &VirtualMachine) -> u64 {
|
||||
vm.state.main_thread_ident.load()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn _is_main_interpreter() -> bool {
|
||||
// RustPython only has one interpreter
|
||||
true
|
||||
}
|
||||
|
||||
/// Initialize the main thread ident. Should be called once at interpreter startup.
|
||||
pub fn init_main_thread_ident(vm: &VirtualMachine) {
|
||||
let ident = get_ident();
|
||||
vm.state.main_thread_ident.store(ident);
|
||||
}
|
||||
|
||||
/// ExceptHookArgs - simple class to hold exception hook arguments
|
||||
/// This allows threading.py to import _excepthook and _ExceptHookArgs from _thread
|
||||
#[pyattr]
|
||||
@@ -532,23 +680,92 @@ pub(crate) mod _thread {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Thread-local storage for cleanup guards
|
||||
// When a thread terminates, the guard is dropped, which triggers cleanup
|
||||
thread_local! {
|
||||
static LOCAL_GUARDS: std::cell::RefCell<Vec<LocalGuard>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
// Guard that removes thread-local data when dropped
|
||||
struct LocalGuard {
|
||||
local: std::sync::Weak<LocalData>,
|
||||
thread_id: std::thread::ThreadId,
|
||||
}
|
||||
|
||||
impl Drop for LocalGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(local_data) = self.local.upgrade() {
|
||||
// Remove from map while holding the lock, but drop the value
|
||||
// outside the lock to prevent deadlock if __del__ accesses _local
|
||||
let removed = local_data.data.lock().remove(&self.thread_id);
|
||||
drop(removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared data structure for Local
|
||||
struct LocalData {
|
||||
data: parking_lot::Mutex<std::collections::HashMap<std::thread::ThreadId, PyDictRef>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LocalData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LocalData").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[pyattr]
|
||||
#[pyclass(module = "thread", name = "_local")]
|
||||
#[pyclass(module = "_thread", name = "_local")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
struct Local {
|
||||
data: ThreadLocal<PyDictRef>,
|
||||
inner: std::sync::Arc<LocalData>,
|
||||
}
|
||||
|
||||
#[pyclass(with(GetAttr, SetAttr), flags(BASETYPE))]
|
||||
impl Local {
|
||||
fn l_dict(&self, vm: &VirtualMachine) -> PyDictRef {
|
||||
self.data.get_or(|| vm.ctx.new_dict()).clone()
|
||||
let thread_id = std::thread::current().id();
|
||||
|
||||
// Fast path: check if dict exists under lock
|
||||
if let Some(dict) = self.inner.data.lock().get(&thread_id).cloned() {
|
||||
return dict;
|
||||
}
|
||||
|
||||
// Slow path: allocate dict outside lock to reduce lock hold time
|
||||
let new_dict = vm.ctx.new_dict();
|
||||
|
||||
// Insert with double-check to handle races
|
||||
let mut data = self.inner.data.lock();
|
||||
use std::collections::hash_map::Entry;
|
||||
let (dict, need_guard) = match data.entry(thread_id) {
|
||||
Entry::Occupied(e) => (e.get().clone(), false),
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(new_dict.clone());
|
||||
(new_dict, true)
|
||||
}
|
||||
};
|
||||
drop(data); // Release lock before TLS access
|
||||
|
||||
// Register cleanup guard only if we inserted a new entry
|
||||
if need_guard {
|
||||
let guard = LocalGuard {
|
||||
local: std::sync::Arc::downgrade(&self.inner),
|
||||
thread_id,
|
||||
};
|
||||
LOCAL_GUARDS.with(|guards| {
|
||||
guards.borrow_mut().push(guard);
|
||||
});
|
||||
}
|
||||
|
||||
dict
|
||||
}
|
||||
|
||||
#[pyslot]
|
||||
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
Self {
|
||||
data: ThreadLocal::new(),
|
||||
inner: std::sync::Arc::new(LocalData {
|
||||
data: parking_lot::Mutex::new(std::collections::HashMap::new()),
|
||||
}),
|
||||
}
|
||||
.into_ref_with_type(vm, cls)
|
||||
.map(Into::into)
|
||||
@@ -597,4 +814,354 @@ pub(crate) mod _thread {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registry of all ThreadHandles for fork cleanup
|
||||
// Stores weak references so handles can be garbage collected normally
|
||||
pub type HandleEntry = (
|
||||
std::sync::Weak<parking_lot::Mutex<ThreadHandleInner>>,
|
||||
std::sync::Weak<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
||||
);
|
||||
|
||||
// Re-export type from vm::thread for PyGlobalState
|
||||
pub use crate::vm::thread::CurrentFrameSlot;
|
||||
|
||||
/// Get all threads' current frames. Used by sys._current_frames().
|
||||
pub fn get_all_current_frames(vm: &VirtualMachine) -> Vec<(u64, FrameRef)> {
|
||||
let registry = vm.state.thread_frames.lock();
|
||||
registry
|
||||
.iter()
|
||||
.filter_map(|(id, slot)| slot.lock().clone().map(|f| (*id, f)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Called after fork() in child process to mark all other threads as done.
|
||||
/// This prevents join() from hanging on threads that don't exist in the child.
|
||||
pub fn after_fork_child(vm: &VirtualMachine) {
|
||||
let current_ident = get_ident();
|
||||
|
||||
// Update main thread ident - after fork, the current thread becomes the main thread
|
||||
vm.state.main_thread_ident.store(current_ident);
|
||||
|
||||
// Reinitialize frame slot for current thread
|
||||
crate::vm::thread::reinit_frame_slot_after_fork(vm);
|
||||
|
||||
// Clean up thread handles if we can acquire the lock.
|
||||
// Use try_lock because the mutex might have been held during fork.
|
||||
// If we can't acquire it, just skip - the child process will work
|
||||
// correctly with new handles it creates.
|
||||
if let Some(mut handles) = vm.state.thread_handles.try_lock() {
|
||||
// Clean up dead weak refs and mark non-current threads as done
|
||||
handles.retain(|(inner_weak, done_event_weak): &HandleEntry| {
|
||||
let Some(inner) = inner_weak.upgrade() else {
|
||||
return false; // Remove dead entries
|
||||
};
|
||||
let Some(done_event) = done_event_weak.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Try to lock the inner state - skip if we can't
|
||||
let Some(mut inner_guard) = inner.try_lock() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Skip current thread and not-started threads
|
||||
if inner_guard.ident == current_ident {
|
||||
return true;
|
||||
}
|
||||
if inner_guard.state == ThreadHandleState::NotStarted {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mark as done and notify waiters
|
||||
inner_guard.state = ThreadHandleState::Done;
|
||||
inner_guard.join_handle = None; // Can't join OS thread from child
|
||||
drop(inner_guard);
|
||||
|
||||
// Try to notify waiters - skip if we can't acquire the lock
|
||||
let (lock, cvar) = &*done_event;
|
||||
if let Some(mut done) = lock.try_lock() {
|
||||
*done = true;
|
||||
cvar.notify_all();
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Thread handle state enum
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThreadHandleState {
|
||||
NotStarted,
|
||||
Starting,
|
||||
Running,
|
||||
Done,
|
||||
}
|
||||
|
||||
// Internal shared state for thread handle
|
||||
pub struct ThreadHandleInner {
|
||||
pub state: ThreadHandleState,
|
||||
pub ident: u64,
|
||||
pub join_handle: Option<thread::JoinHandle<()>>,
|
||||
pub joining: bool, // True if a thread is currently joining
|
||||
pub joined: bool, // Track if join has completed
|
||||
}
|
||||
|
||||
impl fmt::Debug for ThreadHandleInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ThreadHandleInner")
|
||||
.field("state", &self.state)
|
||||
.field("ident", &self.ident)
|
||||
.field("join_handle", &self.join_handle.is_some())
|
||||
.field("joining", &self.joining)
|
||||
.field("joined", &self.joined)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// _ThreadHandle - handle for joinable threads
|
||||
#[pyattr]
|
||||
#[pyclass(module = "_thread", name = "_ThreadHandle")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
struct ThreadHandle {
|
||||
inner: std::sync::Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
||||
// Event to signal thread completion (for timed join support)
|
||||
done_event: std::sync::Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
impl ThreadHandle {
|
||||
fn new(vm: &VirtualMachine) -> Self {
|
||||
let inner = std::sync::Arc::new(parking_lot::Mutex::new(ThreadHandleInner {
|
||||
state: ThreadHandleState::NotStarted,
|
||||
ident: 0,
|
||||
join_handle: None,
|
||||
joining: false,
|
||||
joined: false,
|
||||
}));
|
||||
let done_event =
|
||||
std::sync::Arc::new((parking_lot::Mutex::new(false), parking_lot::Condvar::new()));
|
||||
|
||||
// Register in global registry for fork cleanup
|
||||
vm.state.thread_handles.lock().push((
|
||||
std::sync::Arc::downgrade(&inner),
|
||||
std::sync::Arc::downgrade(&done_event),
|
||||
));
|
||||
|
||||
Self { inner, done_event }
|
||||
}
|
||||
|
||||
#[pygetset]
|
||||
fn ident(&self) -> u64 {
|
||||
self.inner.lock().ident
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn is_done(&self) -> bool {
|
||||
self.inner.lock().state == ThreadHandleState::Done
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn _set_done(&self) {
|
||||
self.inner.lock().state = ThreadHandleState::Done;
|
||||
// Signal waiting threads that this thread is done
|
||||
let (lock, cvar) = &*self.done_event;
|
||||
*lock.lock() = true;
|
||||
cvar.notify_all();
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
fn join(
|
||||
&self,
|
||||
timeout: OptionalArg<Option<Either<f64, i64>>>,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<()> {
|
||||
// Convert timeout to Duration (None or negative = infinite wait)
|
||||
let timeout_duration = match timeout.flatten() {
|
||||
Some(Either::A(t)) if t >= 0.0 => Some(Duration::from_secs_f64(t)),
|
||||
Some(Either::B(t)) if t >= 0 => Some(Duration::from_secs(t as u64)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Check for self-join first
|
||||
{
|
||||
let inner = self.inner.lock();
|
||||
let current_ident = get_ident();
|
||||
if inner.ident == current_ident && inner.state == ThreadHandleState::Running {
|
||||
return Err(vm.new_runtime_error("cannot join current thread".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for thread completion using Condvar (supports timeout)
|
||||
// Loop to handle spurious wakeups
|
||||
let (lock, cvar) = &*self.done_event;
|
||||
let mut done = lock.lock();
|
||||
|
||||
while !*done {
|
||||
if let Some(timeout) = timeout_duration {
|
||||
let result = cvar.wait_for(&mut done, timeout);
|
||||
if result.timed_out() && !*done {
|
||||
// Timeout occurred and done is still false
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// Infinite wait
|
||||
cvar.wait(&mut done);
|
||||
}
|
||||
}
|
||||
drop(done);
|
||||
|
||||
// Thread is done, now perform cleanup
|
||||
let join_handle = {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
// If already joined, return immediately (idempotent)
|
||||
if inner.joined {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If another thread is already joining, wait for them to finish
|
||||
if inner.joining {
|
||||
drop(inner);
|
||||
// Wait on done_event
|
||||
let (lock, cvar) = &*self.done_event;
|
||||
let mut done = lock.lock();
|
||||
while !*done {
|
||||
cvar.wait(&mut done);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Mark that we're joining
|
||||
inner.joining = true;
|
||||
|
||||
// Take the join handle if available
|
||||
inner.join_handle.take()
|
||||
};
|
||||
|
||||
// Perform the actual join outside the lock
|
||||
if let Some(handle) = join_handle {
|
||||
// Ignore the result - panics in spawned threads are already handled
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
// Mark as joined and clear joining flag
|
||||
{
|
||||
let mut inner = self.inner.lock();
|
||||
inner.joined = true;
|
||||
inner.joining = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyslot]
|
||||
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
ThreadHandle::new(vm)
|
||||
.into_ref_with_type(vm, cls)
|
||||
.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
struct StartJoinableThreadArgs {
|
||||
#[pyarg(positional)]
|
||||
function: ArgCallable,
|
||||
#[pyarg(any, optional)]
|
||||
handle: OptionalArg<PyRef<ThreadHandle>>,
|
||||
#[pyarg(any, default = true)]
|
||||
daemon: bool,
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn start_joinable_thread(
|
||||
args: StartJoinableThreadArgs,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<PyRef<ThreadHandle>> {
|
||||
let handle = match args.handle {
|
||||
OptionalArg::Present(h) => h,
|
||||
OptionalArg::Missing => ThreadHandle::new(vm).into_ref(&vm.ctx),
|
||||
};
|
||||
|
||||
// Mark as starting
|
||||
handle.inner.lock().state = ThreadHandleState::Starting;
|
||||
|
||||
// Add non-daemon threads to shutdown registry so _shutdown() will wait for them
|
||||
if !args.daemon {
|
||||
add_to_shutdown_handles(vm, &handle.inner, &handle.done_event);
|
||||
}
|
||||
|
||||
let func = args.function;
|
||||
let handle_clone = handle.clone();
|
||||
let inner_clone = handle.inner.clone();
|
||||
let done_event_clone = handle.done_event.clone();
|
||||
|
||||
let mut thread_builder = thread::Builder::new();
|
||||
let stacksize = vm.state.stacksize.load();
|
||||
if stacksize != 0 {
|
||||
thread_builder = thread_builder.stack_size(stacksize);
|
||||
}
|
||||
|
||||
let join_handle = thread_builder
|
||||
.spawn(vm.new_thread().make_spawn_func(move |vm| {
|
||||
// Set ident and mark as running
|
||||
{
|
||||
let mut inner = inner_clone.lock();
|
||||
inner.ident = get_ident();
|
||||
inner.state = ThreadHandleState::Running;
|
||||
}
|
||||
|
||||
// Ensure cleanup happens even if the function panics
|
||||
let inner_for_cleanup = inner_clone.clone();
|
||||
let done_event_for_cleanup = done_event_clone.clone();
|
||||
let vm_state = vm.state.clone();
|
||||
scopeguard::defer! {
|
||||
// Mark as done
|
||||
inner_for_cleanup.lock().state = ThreadHandleState::Done;
|
||||
|
||||
// Signal waiting threads that this thread is done
|
||||
{
|
||||
let (lock, cvar) = &*done_event_for_cleanup;
|
||||
*lock.lock() = true;
|
||||
cvar.notify_all();
|
||||
}
|
||||
|
||||
// Handle sentinels
|
||||
for lock in SENTINELS.take() {
|
||||
if lock.mu.is_locked() {
|
||||
unsafe { lock.mu.unlock() };
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up thread-local data while VM context is still active
|
||||
cleanup_thread_local_data();
|
||||
|
||||
// Clean up frame tracking
|
||||
crate::vm::thread::cleanup_current_thread_frames(vm);
|
||||
|
||||
vm_state.thread_count.fetch_sub(1);
|
||||
}
|
||||
|
||||
// Run the function
|
||||
match func.invoke((), vm) {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
|
||||
Err(exc) => {
|
||||
vm.run_unraisable(
|
||||
exc,
|
||||
Some("Exception ignored in thread started by".to_owned()),
|
||||
func.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}))
|
||||
.map_err(|err| vm.new_runtime_error(format!("can't start new thread: {err}")))?;
|
||||
|
||||
vm.state.thread_count.fetch_add(1);
|
||||
|
||||
// Store the join handle
|
||||
handle.inner.lock().join_handle = Some(join_handle);
|
||||
|
||||
Ok(handle_clone)
|
||||
}
|
||||
}
|
||||
@@ -110,9 +110,10 @@ impl Interpreter {
|
||||
|
||||
/// Finalize vm and turns an exception to exit code.
|
||||
///
|
||||
/// Finalization steps including 4 steps:
|
||||
/// Finalization steps including 5 steps:
|
||||
/// 1. Flush stdout and stderr.
|
||||
/// 1. Handle exit exception and turn it to exit code.
|
||||
/// 1. Wait for non-daemon threads (threading._shutdown).
|
||||
/// 1. Run atexit exit functions.
|
||||
/// 1. Mark vm as finalized.
|
||||
///
|
||||
@@ -128,6 +129,9 @@ impl Interpreter {
|
||||
0
|
||||
};
|
||||
|
||||
// Wait for non-daemon threads (wait_for_thread_shutdown)
|
||||
wait_for_thread_shutdown(vm);
|
||||
|
||||
atexit::_run_exitfuncs(vm);
|
||||
|
||||
vm.state.finalizing.store(true, Ordering::Release);
|
||||
@@ -139,6 +143,35 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait until threading._shutdown completes, provided
|
||||
/// the threading module was imported in the first place.
|
||||
/// The shutdown routine will wait until all non-daemon
|
||||
/// "threading" threads have completed.
|
||||
fn wait_for_thread_shutdown(vm: &VirtualMachine) {
|
||||
// Try to get the threading module if it was already imported
|
||||
// Use sys.modules.get("threading") like PyImport_GetModule
|
||||
let threading = match (|| -> PyResult<_> {
|
||||
let sys_modules = vm.sys_module.get_attr("modules", vm)?;
|
||||
let threading = sys_modules.get_item("threading", vm)?;
|
||||
Ok(threading)
|
||||
})() {
|
||||
Ok(module) => module,
|
||||
Err(_) => {
|
||||
// threading not imported, nothing to do
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Call threading._shutdown()
|
||||
if let Err(e) = vm.call_method(&threading, "_shutdown", ()) {
|
||||
vm.run_unraisable(
|
||||
e,
|
||||
Some("Exception ignored on threading shutdown".to_owned()),
|
||||
threading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -82,6 +82,10 @@ pub struct VirtualMachine {
|
||||
pub state: PyRc<PyGlobalState>,
|
||||
pub initialized: bool,
|
||||
recursion_depth: Cell<usize>,
|
||||
/// Async generator firstiter hook (per-thread, set via sys.set_asyncgen_hooks)
|
||||
pub async_gen_firstiter: RefCell<Option<PyObjectRef>>,
|
||||
/// Async generator finalizer hook (per-thread, set via sys.set_asyncgen_hooks)
|
||||
pub async_gen_finalizer: RefCell<Option<PyObjectRef>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -107,6 +111,22 @@ pub struct PyGlobalState {
|
||||
pub after_forkers_parent: PyMutex<Vec<PyObjectRef>>,
|
||||
pub int_max_str_digits: AtomicCell<usize>,
|
||||
pub switch_interval: AtomicCell<f64>,
|
||||
/// Global trace function for all threads (set by sys._settraceallthreads)
|
||||
pub global_trace_func: PyMutex<Option<PyObjectRef>>,
|
||||
/// Global profile function for all threads (set by sys._setprofileallthreads)
|
||||
pub global_profile_func: PyMutex<Option<PyObjectRef>>,
|
||||
/// Main thread identifier (pthread_self on Unix)
|
||||
#[cfg(feature = "threading")]
|
||||
pub main_thread_ident: AtomicCell<u64>,
|
||||
/// Registry of all threads' current frames for sys._current_frames()
|
||||
#[cfg(feature = "threading")]
|
||||
pub thread_frames: parking_lot::Mutex<HashMap<u64, stdlib::thread::CurrentFrameSlot>>,
|
||||
/// Registry of all ThreadHandles for fork cleanup
|
||||
#[cfg(feature = "threading")]
|
||||
pub thread_handles: parking_lot::Mutex<Vec<stdlib::thread::HandleEntry>>,
|
||||
/// Registry for non-daemon threads that need to be joined at shutdown
|
||||
#[cfg(feature = "threading")]
|
||||
pub shutdown_handles: parking_lot::Mutex<Vec<stdlib::thread::ShutdownEntry>>,
|
||||
}
|
||||
|
||||
pub fn process_hash_secret_seed() -> u32 {
|
||||
@@ -191,9 +211,21 @@ impl VirtualMachine {
|
||||
after_forkers_parent: PyMutex::default(),
|
||||
int_max_str_digits,
|
||||
switch_interval: AtomicCell::new(0.005),
|
||||
global_trace_func: PyMutex::default(),
|
||||
global_profile_func: PyMutex::default(),
|
||||
#[cfg(feature = "threading")]
|
||||
main_thread_ident: AtomicCell::new(0),
|
||||
#[cfg(feature = "threading")]
|
||||
thread_frames: parking_lot::Mutex::new(HashMap::new()),
|
||||
#[cfg(feature = "threading")]
|
||||
thread_handles: parking_lot::Mutex::new(Vec::new()),
|
||||
#[cfg(feature = "threading")]
|
||||
shutdown_handles: parking_lot::Mutex::new(Vec::new()),
|
||||
}),
|
||||
initialized: false,
|
||||
recursion_depth: Cell::new(0),
|
||||
async_gen_firstiter: RefCell::new(None),
|
||||
async_gen_finalizer: RefCell::new(None),
|
||||
};
|
||||
|
||||
if vm.state.hash_secret.hash_str("")
|
||||
@@ -299,6 +331,10 @@ impl VirtualMachine {
|
||||
panic!("Double Initialize Error");
|
||||
}
|
||||
|
||||
// Initialize main thread ident before any threading operations
|
||||
#[cfg(feature = "threading")]
|
||||
stdlib::thread::init_main_thread_ident(self);
|
||||
|
||||
stdlib::builtins::init_module(self, &self.builtins);
|
||||
stdlib::sys::init_module(self, &self.sys_module, &self.builtins);
|
||||
|
||||
@@ -604,6 +640,9 @@ impl VirtualMachine {
|
||||
) -> PyResult<R> {
|
||||
self.with_recursion("", || {
|
||||
self.frames.borrow_mut().push(frame.clone());
|
||||
// Update the current frame slot for sys._current_frames()
|
||||
#[cfg(feature = "threading")]
|
||||
crate::vm::thread::update_current_frame(Some(frame.clone()));
|
||||
// Push a new exception context for frame isolation
|
||||
// Each frame starts with no active exception (None)
|
||||
// This prevents exceptions from leaking between function calls
|
||||
@@ -613,6 +652,9 @@ impl VirtualMachine {
|
||||
self.pop_exception();
|
||||
// defer dec frame
|
||||
let _popped = self.frames.borrow_mut().pop();
|
||||
// Update the frame slot to the new top frame (or None if empty)
|
||||
#[cfg(feature = "threading")]
|
||||
crate::vm::thread::update_current_frame(self.frames.borrow().last().cloned());
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
use crate::{AsObject, PyObject, PyObjectRef, VirtualMachine};
|
||||
#[cfg(feature = "threading")]
|
||||
use crate::frame::FrameRef;
|
||||
use crate::{AsObject, PyObject, VirtualMachine};
|
||||
use core::{
|
||||
cell::{Cell, RefCell},
|
||||
ptr::NonNull,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
#[cfg(feature = "threading")]
|
||||
use std::sync::Arc;
|
||||
use std::thread_local;
|
||||
|
||||
/// Type for current frame slot - shared between threads for sys._current_frames()
|
||||
#[cfg(feature = "threading")]
|
||||
pub type CurrentFrameSlot = Arc<parking_lot::Mutex<Option<FrameRef>>>;
|
||||
|
||||
thread_local! {
|
||||
pub(super) static VM_STACK: RefCell<Vec<NonNull<VirtualMachine>>> = Vec::with_capacity(1).into();
|
||||
|
||||
pub(crate) static COROUTINE_ORIGIN_TRACKING_DEPTH: Cell<u32> = const { Cell::new(0) };
|
||||
pub(crate) static ASYNC_GEN_FINALIZER: RefCell<Option<PyObjectRef>> = const { RefCell::new(None) };
|
||||
pub(crate) static ASYNC_GEN_FIRSTITER: RefCell<Option<PyObjectRef>> = const { RefCell::new(None) };
|
||||
|
||||
/// Current thread's frame slot for sys._current_frames()
|
||||
#[cfg(feature = "threading")]
|
||||
static CURRENT_FRAME_SLOT: RefCell<Option<CurrentFrameSlot>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
scoped_tls::scoped_thread_local!(static VM_CURRENT: VirtualMachine);
|
||||
@@ -26,11 +36,74 @@ pub fn with_current_vm<R>(f: impl FnOnce(&VirtualMachine) -> R) -> R {
|
||||
pub fn enter_vm<R>(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R {
|
||||
VM_STACK.with(|vms| {
|
||||
vms.borrow_mut().push(vm.into());
|
||||
|
||||
// Initialize frame slot for this thread if not already done
|
||||
#[cfg(feature = "threading")]
|
||||
init_frame_slot_if_needed(vm);
|
||||
|
||||
scopeguard::defer! { vms.borrow_mut().pop(); }
|
||||
VM_CURRENT.set(vm, f)
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize frame slot for current thread if not already initialized.
|
||||
/// Called automatically by enter_vm().
|
||||
#[cfg(feature = "threading")]
|
||||
fn init_frame_slot_if_needed(vm: &VirtualMachine) {
|
||||
CURRENT_FRAME_SLOT.with(|slot| {
|
||||
if slot.borrow().is_none() {
|
||||
let thread_id = crate::stdlib::thread::get_ident();
|
||||
let new_slot = Arc::new(parking_lot::Mutex::new(None));
|
||||
vm.state
|
||||
.thread_frames
|
||||
.lock()
|
||||
.insert(thread_id, new_slot.clone());
|
||||
*slot.borrow_mut() = Some(new_slot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update the current thread's frame. Called when frames are pushed/popped.
|
||||
/// This is a hot path - uses only thread-local storage, no locks.
|
||||
#[cfg(feature = "threading")]
|
||||
pub fn update_current_frame(frame: Option<FrameRef>) {
|
||||
CURRENT_FRAME_SLOT.with(|slot| {
|
||||
if let Some(s) = slot.borrow().as_ref() {
|
||||
*s.lock() = frame;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Cleanup frame tracking for the current thread. Called at thread exit.
|
||||
#[cfg(feature = "threading")]
|
||||
pub fn cleanup_current_thread_frames(vm: &VirtualMachine) {
|
||||
let thread_id = crate::stdlib::thread::get_ident();
|
||||
vm.state.thread_frames.lock().remove(&thread_id);
|
||||
CURRENT_FRAME_SLOT.with(|s| {
|
||||
*s.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
|
||||
/// Reinitialize frame slot after fork. Called in child process.
|
||||
/// Creates a fresh slot and registers it for the current thread.
|
||||
#[cfg(feature = "threading")]
|
||||
pub fn reinit_frame_slot_after_fork(vm: &VirtualMachine) {
|
||||
let current_ident = crate::stdlib::thread::get_ident();
|
||||
let new_slot = Arc::new(parking_lot::Mutex::new(None));
|
||||
|
||||
// Try to update the global registry. If we can't get the lock
|
||||
// (parent thread might have been holding it during fork), skip.
|
||||
if let Some(mut registry) = vm.state.thread_frames.try_lock() {
|
||||
registry.clear();
|
||||
registry.insert(current_ident, new_slot.clone());
|
||||
}
|
||||
|
||||
// Always update thread-local to point to the new slot
|
||||
CURRENT_FRAME_SLOT.with(|s| {
|
||||
*s.borrow_mut() = Some(new_slot);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn with_vm<F, R>(obj: &PyObject, f: F) -> Option<R>
|
||||
where
|
||||
F: Fn(&VirtualMachine) -> R,
|
||||
@@ -139,6 +212,10 @@ impl VirtualMachine {
|
||||
/// specific guaranteed behavior.
|
||||
#[cfg(feature = "threading")]
|
||||
pub fn new_thread(&self) -> ThreadedVirtualMachine {
|
||||
let global_trace = self.state.global_trace_func.lock().clone();
|
||||
let global_profile = self.state.global_profile_func.lock().clone();
|
||||
let use_tracing = global_trace.is_some() || global_profile.is_some();
|
||||
|
||||
let vm = Self {
|
||||
builtins: self.builtins.clone(),
|
||||
sys_module: self.sys_module.clone(),
|
||||
@@ -147,9 +224,9 @@ impl VirtualMachine {
|
||||
wasm_id: self.wasm_id.clone(),
|
||||
exceptions: RefCell::default(),
|
||||
import_func: self.import_func.clone(),
|
||||
profile_func: RefCell::new(self.ctx.none()),
|
||||
trace_func: RefCell::new(self.ctx.none()),
|
||||
use_tracing: Cell::new(false),
|
||||
profile_func: RefCell::new(global_profile.unwrap_or_else(|| self.ctx.none())),
|
||||
trace_func: RefCell::new(global_trace.unwrap_or_else(|| self.ctx.none())),
|
||||
use_tracing: Cell::new(use_tracing),
|
||||
recursion_limit: self.recursion_limit.clone(),
|
||||
signal_handlers: None,
|
||||
signal_rx: None,
|
||||
@@ -157,6 +234,8 @@ impl VirtualMachine {
|
||||
state: self.state.clone(),
|
||||
initialized: self.initialized,
|
||||
recursion_depth: Cell::new(0),
|
||||
async_gen_firstiter: RefCell::new(None),
|
||||
async_gen_finalizer: RefCell::new(None),
|
||||
};
|
||||
ThreadedVirtualMachine { vm }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user