diff options
Diffstat (limited to 'Lib')
| -rw-r--r-- | Lib/test/test_thread.py | 91 | ||||
| -rw-r--r-- | Lib/threading.py | 24 |
2 files changed, 54 insertions, 61 deletions
diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index 931cb4b..8323523 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -189,8 +189,8 @@ class ThreadRunningTests(BasicThreadTest): with threading_helper.wait_threads_exit(): handle = thread.start_joinable_thread(task) handle.join() - with self.assertRaisesRegex(ValueError, "not joinable"): - handle.join() + # Subsequent join() calls should succeed + handle.join() def test_joinable_not_joined(self): handle_destroyed = thread.allocate_lock() @@ -233,58 +233,61 @@ class ThreadRunningTests(BasicThreadTest): with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): raise errors[0] - def test_detach_from_self(self): - errors = [] - handles = [] - start_joinable_thread_returned = thread.allocate_lock() - start_joinable_thread_returned.acquire() - thread_detached = thread.allocate_lock() - thread_detached.acquire() + def test_join_then_self_join(self): + # make sure we can't deadlock in the following scenario with + # threads t0 and t1 (see comment in `ThreadHandle_join()` for more + # details): + # + # - t0 joins t1 + # - t1 self joins + def make_lock(): + lock = thread.allocate_lock() + lock.acquire() + return lock + + error = None + self_joiner_handle = None + self_joiner_started = make_lock() + self_joiner_barrier = make_lock() + def self_joiner(): + nonlocal error + + self_joiner_started.release() + self_joiner_barrier.acquire() - def task(): - start_joinable_thread_returned.acquire() try: - handles[0].detach() + self_joiner_handle.join() except Exception as e: - errors.append(e) - finally: - thread_detached.release() + error = e + + joiner_started = make_lock() + def joiner(): + joiner_started.release() + self_joiner_handle.join() with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - handles.append(handle) - start_joinable_thread_returned.release() - thread_detached.acquire() - with self.assertRaisesRegex(ValueError, "not joinable"): - handle.join() + self_joiner_handle = thread.start_joinable_thread(self_joiner) + # Wait for the self-joining thread to start + self_joiner_started.acquire() - assert len(errors) == 0 + # Start the thread that joins the self-joiner + joiner_handle = thread.start_joinable_thread(joiner) - def test_detach_then_join(self): - lock = thread.allocate_lock() - lock.acquire() + # Wait for the joiner to start + joiner_started.acquire() - def task(): - lock.acquire() + # Not great, but I don't think there's a deterministic way to make + # sure that the self-joining thread has been joined. + time.sleep(0.1) - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - # detach() returns even though the thread is blocked on lock - handle.detach() - # join() then cannot be called anymore - with self.assertRaisesRegex(ValueError, "not joinable"): - handle.join() - lock.release() - - def test_join_then_detach(self): - def task(): - pass + # Unblock the self-joiner + self_joiner_barrier.release() - with threading_helper.wait_threads_exit(): - handle = thread.start_joinable_thread(task) - handle.join() - with self.assertRaisesRegex(ValueError, "not joinable"): - handle.detach() + self_joiner_handle.join() + joiner_handle.join() + + with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"): + raise error class Barrier: diff --git a/Lib/threading.py b/Lib/threading.py index b6ff00a..ec89550 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -931,7 +931,6 @@ class Thread: if _HAVE_THREAD_NATIVE_ID: self._native_id = None self._tstate_lock = None - self._join_lock = None self._handle = None self._started = Event() self._is_stopped = False @@ -956,14 +955,11 @@ class Thread: if self._tstate_lock is not None: self._tstate_lock._at_fork_reinit() self._tstate_lock.acquire() - if self._join_lock is not None: - self._join_lock._at_fork_reinit() else: # This thread isn't alive after fork: it doesn't have a tstate # anymore. self._is_stopped = True self._tstate_lock = None - self._join_lock = None self._handle = None def __repr__(self): @@ -996,8 +992,6 @@ class Thread: if self._started.is_set(): raise RuntimeError("threads can only be started once") - self._join_lock = _allocate_lock() - with _active_limbo_lock: _limbo[self] = self try: @@ -1167,17 +1161,9 @@ class Thread: self._join_os_thread() def _join_os_thread(self): - join_lock = self._join_lock - if join_lock is None: - return - with join_lock: - # Calling join() multiple times would raise an exception - # in one of the callers. - if self._handle is not None: - self._handle.join() - self._handle = None - # No need to keep this around - self._join_lock = None + # self._handle may be cleared post-fork + if self._handle is not None: + self._handle.join() def _wait_for_tstate_lock(self, block=True, timeout=-1): # Issue #18808: wait for the thread state to be gone. @@ -1478,6 +1464,10 @@ class _MainThread(Thread): with _active_limbo_lock: _active[self._ident] = self + def _join_os_thread(self): + # No ThreadHandle for main thread + pass + # Helper thread-local instance to detect when a _DummyThread # is collected. Not a part of the public API. |
