Skip to content

gh-114271: Make _thread.ThreadHandle thread-safe in free-threaded builds - #115190

Merged
gpshead merged 21 commits into
python:mainfrom
mpage:gh-114271-_thread-ThreadHandle
Mar 1, 2024
Merged

gh-114271: Make _thread.ThreadHandle thread-safe in free-threaded builds#115190
gpshead merged 21 commits into
python:mainfrom
mpage:gh-114271-_thread-ThreadHandle

Conversation

@mpage

@mpagempage commented Feb 8, 2024

Copy link
Copy Markdown
Contributor

We protect the mutable state of ThreadHandle using a _PyOnceFlag. Concurrent operations (i.e. join) on ThreadHandle block until it is their turn to execute or an earlier operation succeeds. Once an operation has been applied successfully all future operations complete immediately.

The join() method is now idempotent. It may be called multiple times but the underlying OS thread will only be joined once. After join() succeeds, any future calls to join() will succeed immediately.

The detach() method has been removed; nothing was using it.

We protect the mutable state of `ThreadHandle` using a `_PyOnceFlag`.
Concurrent operations (i.e. `join` or `detach`) on `ThreadHandle` block
until it is their turn to execute or an earlier operation succeeds.
Once an operation has been applied successfully all future operations
complete immediately.
The `join()` method is now idempotent. It may be called multiple times
but the underlying OS thread will only be joined once. After `join()`
succeeds, any future calls to `join()` will succeed immediately.
The `detach()` method is also idempotent. It may be called multiple times
but the underlying OS thread will only be detached once. After `detach()`
succeeds, any future calls to `detach()` will succeed immediately.
If the handle is being joined, `detach()` blocks until the join completes.
@mpage
mpage marked this pull request as ready for review February 9, 2024 00:55
@mpage

mpage commented Feb 9, 2024

Copy link
Copy Markdown
ContributorAuthor

@colesbury - Would you please have a look and add the "skip news" label to this? In order to limit the size of the changes I'd like to get this merged first and then stack the tstate_lock stuff on top.

Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c Outdated
@mpage

mpage commented Feb 9, 2024

Copy link
Copy Markdown
ContributorAuthor

@colesbury - This should be good to go. Would you please have another look?

@mpagempage mentioned this pull request Feb 9, 2024

@colesburycolesbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, the thread id reuse is tricky.

I'd like to avoid adding _PyEventRc if possible. The event doesn't seem like a great fit for ThreadHandleObject because we are never waiting on the event -- we're just using it like an atomic flag.

What about something like:

  1. Make thread_is_exiting a plain variable that we use atomic operations to access, like int thread_is_exiting (because we don't have atomic ops on bool currently)
  2. Pass the ThreadHandleObject to do_start_new_thread instead of having another object with a separate lifetime.

@mpage

mpage commented Feb 9, 2024

Copy link
Copy Markdown
ContributorAuthor

I'd like to avoid adding _PyEventRc if possible.

Me too! I thought pretty hard about this and couldn't come up with a better solution. Plus, I think we're going to need it later anyway if we want to be able to implement join with a timeout, at which point we will be waiting on it.

What about something like: ...

I considered this, but I think we need to set the flag after the PyThreadState has been destroyed. Otherwise, you could end up deadlocking in the following situation:

  1. T0 joins T1.
  2. T1 finishes. It has a thread-local with a finalizer that joins T1. The flag is set at this point, so we skip the self-join check and deadlock.

Since we have to set the flag after the PyThreadState has been destroyed, it's no longer safe to call Py_DECREF, so we can't use a ThreadHandleObject (or any Python object). We could create a ref-counted flag (or re-use one if it exists), but since we're already going to need the _PyEventRc anyway, I figured we'd just use it here.

@colesbury
colesbury requested a review from pitrouFebruary 9, 2024 23:32

@colesburycolesbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me.

@pitrou, would you please look this over? The motivation is to make thread joining thread-safe in the freethreaded build, as well as fix a race condition that can affect both the default and freethreaded build (described by @mpage in #114839)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general this looks ok but I think this can be simplified, see below.

Comment threadInclude/internal/pycore_lock.h Outdated
Comment threadModules/_threadmodule.c
Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c
Comment threadModules/_threadmodule.c
Comment threadModules/_threadmodule.c
@mpage

mpage commented Feb 13, 2024

Copy link
Copy Markdown
ContributorAuthor

@pitrou - I think I've addressed all of your comments except for the question about using bool. A quick grep of the codebase reveals quite a few uses of bool (in central places like obmalloc.c and dictobject.c, among others). Even if there isn't explicit guidance, there's certainly precedent. I've also removed the detach() method. Would you please have another look?

@colesbury - This has changed a bit since you reviewed it. Would you please have another look? In particular, I'd like a sanity check that the use of relaxed atomics for accessing handle state is OK.

@mpage

Copy link
Copy Markdown
ContributorAuthor

The failing macos-13 builds appear unrelated to this PR. They're failing with errors like:

error: call to undeclared function 'is_pad'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
if (py_is_pad(self->win)) {

@colesburycolesbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few minor comments below

Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c Outdated
Comment threadModules/_threadmodule.c Outdated
@mpage

Copy link
Copy Markdown
ContributorAuthor

@pitrou - I think I've addressed everything now. Would you please have a look?

@pitrou
pitrou requested a review from gpsheadFebruary 14, 2024 16:47
@mpage

Copy link
Copy Markdown
ContributorAuthor

@pitrou@gpshead - Did you want to take a look at this?

@gpsheadgpshead left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(review in progress, more to come)

with threading_helper.wait_threads_exit():
handle = thread.start_joinable_thread(task)
handle.join()
with self.assertRaisesRegex(ValueError, "not joinable"):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is interesting... our docs already claim that threads can be joined multiple times. So I wonder why this existing test logic was previously explicitly checking for an error here.

In this sense this change aligns with what our docs claim so a behavior change here could be seen as a bugfix. I do not expect anyone to be depending on subsequent join()s of a thread raising regardless.

Comment threadLib/test/test_thread.py
Comment threadLib/test/test_thread.py
Comment threadLib/threading.py
@bedevere-app

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Comment threadModules/_threadmodule.c
Comment threadLib/test/test_thread.py
Comment threadModules/_threadmodule.c
Comment threadModules/_threadmodule.c
Comment threadModules/_threadmodule.c
@gpsheadgpshead self-assigned this Feb 26, 2024
@mpage

Copy link
Copy Markdown
ContributorAuthor

I have made the requested changes; please review again

@mpage
mpage requested a review from gpsheadFebruary 27, 2024 23:54
@mpage

Copy link
Copy Markdown
ContributorAuthor

@gpshead - Would you please have another look at this?

Comment threadModules/_threadmodule.c
@gpshead
gpshead merged commit 9e88173 into python:mainMar 1, 2024
woodruffw pushed a commit to woodruffw-forks/cpython that referenced this pull request Mar 4, 2024
…aded builds (pythonGH-115190)
Make `_thread.ThreadHandle` thread-safe in free-threaded builds
We protect the mutable state of `ThreadHandle` using a `_PyOnceFlag`.
Concurrent operations (i.e. `join` or `detach`) on `ThreadHandle` block
until it is their turn to execute or an earlier operation succeeds.
Once an operation has been applied successfully all future operations
complete immediately.
The `join()` method is now idempotent. It may be called multiple times
but the underlying OS thread will only be joined once. After `join()`
succeeds, any future calls to `join()` will succeed immediately.
The internal thread handle `detach()` method has been removed.
adorilson pushed a commit to adorilson/cpython that referenced this pull request Mar 25, 2024
…aded builds (pythonGH-115190)
Make `_thread.ThreadHandle` thread-safe in free-threaded builds
We protect the mutable state of `ThreadHandle` using a `_PyOnceFlag`.
Concurrent operations (i.e. `join` or `detach`) on `ThreadHandle` block
until it is their turn to execute or an earlier operation succeeds.
Once an operation has been applied successfully all future operations
complete immediately.
The `join()` method is now idempotent. It may be called multiple times
but the underlying OS thread will only be joined once. After `join()`
succeeds, any future calls to `join()` will succeed immediately.
The internal thread handle `detach()` method has been removed.
diegorusso pushed a commit to diegorusso/cpython that referenced this pull request Apr 17, 2024
…aded builds (pythonGH-115190)
Make `_thread.ThreadHandle` thread-safe in free-threaded builds
We protect the mutable state of `ThreadHandle` using a `_PyOnceFlag`.
Concurrent operations (i.e. `join` or `detach`) on `ThreadHandle` block
until it is their turn to execute or an earlier operation succeeds.
Once an operation has been applied successfully all future operations
complete immediately.
The `join()` method is now idempotent. It may be called multiple times
but the underlying OS thread will only be joined once. After `join()`
succeeds, any future calls to `join()` will succeed immediately.
The internal thread handle `detach()` method has been removed.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@mpage@gpshead@colesbury@pitrou