GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-114271: Fix race in `Thread.join()` by mpage · Pull Request #114839 · python/cpython

Conversation

@mpage

@mpage mpage commented Feb 1, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

This PR makes thread joining thread-safe in free-threaded builds and fixes gh-116372, which affects both default and free-threaded builds.

The combination of _tstate_lock and _ThreadHandle is replaced by _ThreadHandle and the supporting infrastructure for _tstate_lock is removed. Joining a thread now joins its _ThreadHandle. The _ThreadHandles for all threads that we want to join on shutdown (non-daemon threads that were created by the threading module) are tracked in a linked-list attached to the _thread module. At shutdown we iterate over this list and join the handles.

There is one wrinkle with this approach: the _MainThread instance. The _MainThread instance is joinable. Previously, it represented whatever thread imported the threading module. Typically this is the main thread of the main interpreter, but in some rare circumstances (embedding scenarios?) it might not be. The previous approach allowed the MainThread instance to be joined even if it wasn't the main thread of the main interpreter, but this isn't possible with the current approach. To work around this we change the _MainThread to always represent the main thread of the runtime, also fixing gh-83223.

  • Issue: Make _threadmodule.c thread-safe in --disable-gil builds #114271

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

stonebig reacted with heart emoji
All reactions
  • ❤️ 1 reaction
@mpage
Fix race in Thread.join()
fb13cf6
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]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1201
[^2]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1115
[^3]: 8194653
@bedevere-app bedevere-app Bot mentioned this pull request Feb 1, 2024
Make _threadmodule.c thread-safe in --disable-gil builds #114271
Closed
blurb-it Bot and others added 2 commits February 1, 2024 03:09
@blurb-it
📜🤖 Added by blurb_it.
b093e5d
@mpage
Fix NEWS entry
1039208
@Eclips4
Eclips4 requested a review from colesbury February 1, 2024 05:55
@Eclips4 Eclips4 added the topic-free-threading label Feb 1, 2024
@mpage

mpage commented Feb 1, 2024

Copy link
Copy Markdown
Contributor Author

Just a heads up - please don't look at this until it's marked as ready for review. There are a couple of failing CI jobs that need to be addressed.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mpage mpage mentioned this pull request Feb 3, 2024
Race between _PyParkingLot_Park and _PyParkingLot_UnparkAll when handling interrupts #114944
Closed
mpage added 2 commits February 5, 2024 13:26
@mpage
Work around c-analyzer limitation
196081d
When we have `typedef struct _PyEventRc _PyEventRc` in `pstate.h`
the C analyzer can't decide between that and the definition in `pycore_lock.h`.
@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
19d7af1
@mpage
mpage marked this pull request as ready for review February 5, 2024 22:49
@mpage
mpage requested a review from ericsnowcurrently as a code owner February 5, 2024 22:49
@bedevere-app bedevere-app Bot added the awaiting review label Feb 5, 2024
@mpage

mpage commented Feb 6, 2024

Copy link
Copy Markdown
Contributor Author

@colesbury - This is good to go. Would you please have a look?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mpage
mpage marked this pull request as draft February 6, 2024 19:52
@bedevere-app bedevere-app Bot removed the awaiting review label Feb 6, 2024
@mpage

mpage commented Feb 6, 2024

Copy link
Copy Markdown
Contributor Author

Putting this back in draft. Going to see if I can move the join logic into ThreadHandle.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
c86d349
@colesbury colesbury mentioned this pull request Feb 9, 2024
gh-114271: Make _thread.ThreadHandle thread-safe in free-threaded builds #115190
Merged
mpage added 7 commits February 13, 2024 20:20
@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
40e4b36
@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
4ed1083
@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
34b6065
@mpage
Fix two compilation errors post merge
1c82786
@mpage
Use ThreadHandle as the single abstraction for thread joining
0e86cf9
@mpage
Isolate all logic in _threadmodule
76bde03
@mpage
Fix flag
02123b8
@mpage
mpage marked this pull request as ready for review March 5, 2024 20:30
@bedevere-app bedevere-app Bot added the awaiting review label Mar 5, 2024
@mpage

mpage commented Mar 6, 2024

Copy link
Copy Markdown
Contributor Author

@colesbury - Could you take a look at this when you get a chance, please?

colesbury reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

colesbury
colesbury reviewed Mar 6, 2024
View reviewed changes

@colesbury colesbury left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the approach looks nice!

I'm still looking this over, but I have some questions about thread._handle. Currently, thread._handle starts out as None and is then set to a ThreadHandle after the thread is started. This None -> ThreadHandle transition seems like it adds complexity and I think there may be a few bugs related to it (see comments below).

Would it be possible to always have a non-None self._handle by pushing a bit more state into ThreadHandle?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/threading.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Python/pystate.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

@ericsnowcurrently

ericsnowcurrently commented Mar 14, 2024

Copy link
Copy Markdown
Member

I'd be glad to follow-through on the changes I posted in gh-116776, if that would help you.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mpage

mpage commented Mar 14, 2024

Copy link
Copy Markdown
Contributor Author

I'd be glad to follow-through on the changes I posted in #116776, if that would help you.

@ericsnowcurrently - Thanks! Is that something you think you can get done quickly (i.e. in the next few days)? If so, I'd be happy to stack on top of those changes once you're done.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ericsnowcurrently

ericsnowcurrently commented Mar 14, 2024

Copy link
Copy Markdown
Member

Yeah, I'll aim to wrap it up tomorrow.

mpage reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

pitrou
pitrou approved these changes Mar 15, 2024
View reviewed changes

@pitrou pitrou left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. I really like the simplification here!

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mpage reacted with heart emoji
All reactions
  • ❤️ 1 reaction
Comment thread Modules/_threadmodule.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Modules/_threadmodule.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Modules/_threadmodule.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting changes labels Mar 15, 2024
@ericsnowcurrently

ericsnowcurrently commented Mar 15, 2024

Copy link
Copy Markdown
Member

I'll defer to @pitrou's assessment on this. I'd rather this land sooner than later and don't want my pace to hold things up. Again, thanks for working on this.

(I'll follow up separately on any cleanup, about the "main" thread, and about our approach to managing thread lifetimes. I actually appreciate not feeling so rushed to sort those out. 😄)

mpage reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ericsnowcurrently

ericsnowcurrently commented Mar 15, 2024

Copy link
Copy Markdown
Member

FWIW, this change feels a bit closer to what I had in mind last year: #110848 (comment).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

mpage added 3 commits March 15, 2024 09:20
@mpage
Merge branch 'main' into pythongh-114271-remove-tstate_lock
ee96259
@mpage
Use infinitive in docstring for _shutdown
dc57ed2
@mpage
Update comment for _make_thread_handle
48b86ae
@mpage

mpage commented Mar 15, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

The CIFuzz (memory) failure is unrelated to this PR.

pitrou reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pitrou

pitrou commented Mar 15, 2024 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Do we have a Thread Sanitizer CI build somewhere? In my experience it's quite good at finding synchronization bugs.
@gpshead

Edit: oh, I see this is being proposed as #116872. Thank you @corona10 !

corona10 reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pitrou

pitrou commented Mar 15, 2024

Copy link
Copy Markdown
Member

@gpshead Would you like to take a look at this?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pitrou
Merge branch 'main' into pythongh-114271-remove-tstate_lock
9a8ea9b
@pitrou

pitrou commented Mar 16, 2024

Copy link
Copy Markdown
Member

Ok, TSAN tests passed after merging from main (though there are too few of them currently).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pitrou
Merge branch 'main' into pythongh-114271-remove-tstate_lock
339b2e6
@pitrou

pitrou commented Mar 16, 2024

Copy link
Copy Markdown
Member

Updated again to get test_threading in the set of TSAN tests.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@pitrou
pitrou merged commit 33da0e8 into python:main Mar 16, 2024
@bedevere-app bedevere-app Bot removed the awaiting merge label Mar 16, 2024
vstinner pushed a commit to vstinner/cpython that referenced this pull request Mar 20, 2024
@mpage @blurb-it
@pitrou @vstinner
pythongh-114271: Fix race in Thread.join() (python#114839)
a71f637
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]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1201
[^2]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1115
[^3]: mpage@8194653

---------

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Antoine Pitrou <[email protected]>
adorilson pushed a commit to adorilson/cpython that referenced this pull request Mar 25, 2024
@mpage @blurb-it
@pitrou @adorilson
pythongh-114271: Fix race in Thread.join() (python#114839)
1a0dba1
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]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1201
[^2]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1115
[^3]: mpage@8194653

---------

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Antoine Pitrou <[email protected]>
diegorusso pushed a commit to diegorusso/cpython that referenced this pull request Apr 17, 2024
@mpage @blurb-it
@pitrou @diegorusso
pythongh-114271: Fix race in Thread.join() (python#114839)
faaa555
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]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1201
[^2]: https://github.com/python/cpython/blob/441affc9e7f419ef0b68f734505fa2f79fe653c7/Lib/threading.py#L1115
[^3]: mpage@8194653

---------

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Antoine Pitrou <[email protected]>
@pajod pajod mentioned this pull request May 14, 2024
API calls getting stuck indefinitely possibiliy due to error "gevent.exceptions.LoopExit" benoitc/gunicorn#3117
Closed
@aisk aisk mentioned this pull request Jun 4, 2024
Use the runtime's main thread ID in the threading module. #83223
Open
intellij-monorepo-bot pushed a commit to JetBrains/intellij-community that referenced this pull request Jun 21, 2024
@lancelote @intellij-monorepo-bot
PY-73047 Use Thread.is_alive instead of Thread._is_stopped in Pyt…
2efd9fd
…hon 3.13

`Thread._is_stopped` was removed in python/cpython#114839 in Python 3.13

(cherry picked from commit d568bc288aed00268ffeef137b9b901f480964ef)

IJ-MR-137093

GitOrigin-RevId: aa8b27b09db85f69b44d6fbbd333f8ec32489d33
@rominf rominf mentioned this pull request Jul 5, 2024
Test failure with Python 3.13 gevent/gevent#2037
Closed
@hauntsaninja hauntsaninja mentioned this pull request Sep 15, 2024
Daemonic threads not killed in some circumstances in python 3.13 #123940
Closed
@ericsnowcurrently ericsnowcurrently mentioned this pull request Sep 17, 2024
gh-123940: Ensure force-terminated daemon threads can be joined #124150
Closed
@vfazio vfazio mentioned this pull request Feb 14, 2025
Active thread list may be inaccurate due to data type mismatch #130115
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@colesbury colesbury colesbury approved these changes
@ericsnowcurrently ericsnowcurrently ericsnowcurrently requested changes
@pitrou pitrou pitrou approved these changes
@gpshead gpshead Awaiting requested review from gpshead

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

topic-free-threading

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Race in Thread.join()

Uh oh!

There was an error while loading. Please reload this page.

5 participants

@mpage @ericsnowcurrently @pitrou @colesbury @Eclips4
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.