Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 21 commits into from
Mar 1, 2024

Conversation

mpage
Copy link
Contributor

@mpage mpage commented Feb 8, 2024

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
Copy link
Contributor Author

mpage commented Feb 9, 2024

@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.

Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
@mpage
Copy link
Contributor Author

mpage commented Feb 9, 2024

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

Copy link
Contributor

@colesbury colesbury left a comment

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
Copy link
Contributor Author

mpage commented Feb 9, 2024

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 pitrou February 9, 2024 23:32
Copy link
Contributor

@colesbury colesbury left a comment

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)

Copy link
Member

@pitrou pitrou left a comment

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.

Include/internal/pycore_lock.h Outdated Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
@mpage
Copy link
Contributor Author

mpage commented Feb 13, 2024

@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
Contributor Author

mpage commented Feb 13, 2024

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)) {

Copy link
Contributor

@colesbury colesbury left a comment

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

Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
Modules/_threadmodule.c Outdated Show resolved Hide resolved
@mpage
Copy link
Contributor Author

mpage commented Feb 14, 2024

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

@mpage
Copy link
Contributor Author

mpage commented Feb 26, 2024

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

Copy link
Member

@gpshead gpshead left a comment

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)

@@ -189,8 +189,8 @@ def task():
with threading_helper.wait_threads_exit():
handle = thread.start_joinable_thread(task)
handle.join()
with self.assertRaisesRegex(ValueError, "not joinable"):
Copy link
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.

Lib/test/test_thread.py Show resolved Hide resolved
Lib/test/test_thread.py Show resolved Hide resolved
Lib/threading.py Show resolved Hide resolved
@bedevere-app
Copy link

bedevere-app bot commented Feb 26, 2024

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.

Modules/_threadmodule.c Outdated Show resolved Hide resolved
Lib/test/test_thread.py Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
Modules/_threadmodule.c Show resolved Hide resolved
@gpshead gpshead self-assigned this Feb 26, 2024
@mpage
Copy link
Contributor Author

mpage commented Feb 27, 2024

I have made the requested changes; please review again

@mpage mpage requested a review from gpshead February 27, 2024 23:54
@mpage
Copy link
Contributor Author

mpage commented Feb 29, 2024

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

@gpshead gpshead merged commit 9e88173 into python:main Mar 1, 2024
33 of 34 checks passed
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 free to 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