mirror of
https://github.com/python/cpython.git
synced 2025-11-02 19:12:55 +00:00
gh-114271: Fix race in Thread.join() (#114839)
There is a race between when `Thread._tstate_lock` is released[^1] in `Thread._wait_for_tstate_lock()` and when `Thread._stop()` asserts[^2] that it is unlocked. Consider the following execution involving threads A, B, and C: 1. A starts. 2. B joins A, blocking on its `_tstate_lock`. 3. C joins A, blocking on its `_tstate_lock`. 4. A finishes and releases its `_tstate_lock`. 5. B acquires A's `_tstate_lock` in `_wait_for_tstate_lock()`, releases it, but is swapped out before calling `_stop()`. 6. C is scheduled, acquires A's `_tstate_lock` in `_wait_for_tstate_lock()` but is swapped out before releasing it. 7. B is scheduled, calls `_stop()`, which asserts that A's `_tstate_lock` is not held. However, C holds it, so the assertion fails. The race can be reproduced[^3] by inserting sleeps at the appropriate points in the threading code. To do so, run the `repro_join_race.py` from the linked repo. There are two main parts to this PR: 1. `_tstate_lock` is replaced with an event that is attached to `PyThreadState`. The event is set by the runtime prior to the thread being cleared (in the same place that `_tstate_lock` was released). `Thread.join()` blocks waiting for the event to be set. 2. `_PyInterpreterState_WaitForThreads()` provides the ability to wait for all non-daemon threads to exit. To do so, an `is_daemon` predicate was added to `PyThreadState`. This field is set each time a thread is created. `threading._shutdown()` now calls into `_PyInterpreterState_WaitForThreads()` instead of waiting on `_tstate_lock`s. [^1]:441affc9e7/Lib/threading.py (L1201)[^2]:441affc9e7/Lib/threading.py (L1115)[^3]:8194653279--------- Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Antoine Pitrou <antoine@python.org>
This commit is contained in:
parent
86bc40dd41
commit
33da0e844c
12 changed files with 771 additions and 643 deletions
|
|
@ -153,16 +153,6 @@ PyAPI_FUNC(void) PyEvent_Wait(PyEvent *evt);
|
|||
// and 0 if the timeout expired or thread was interrupted.
|
||||
PyAPI_FUNC(int) PyEvent_WaitTimed(PyEvent *evt, PyTime_t timeout_ns);
|
||||
|
||||
// A one-time event notification with reference counting.
|
||||
typedef struct _PyEventRc {
|
||||
PyEvent event;
|
||||
Py_ssize_t refcount;
|
||||
} _PyEventRc;
|
||||
|
||||
_PyEventRc *_PyEventRc_New(void);
|
||||
void _PyEventRc_Incref(_PyEventRc *erc);
|
||||
void _PyEventRc_Decref(_PyEventRc *erc);
|
||||
|
||||
// _PyRawMutex implements a word-sized mutex that that does not depend on the
|
||||
// parking lot API, and therefore can be used in the parking lot
|
||||
// implementation.
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ struct _pythread_runtime_state {
|
|||
} stubs;
|
||||
#endif
|
||||
|
||||
// Linked list of ThreadHandleObjects
|
||||
// Linked list of ThreadHandles
|
||||
struct llist_node handles;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue