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

fix: make gil_safe_call_once thread-safe in free-threaded CPython #5246

Merged
merged 3 commits into from
Jul 16, 2024

Conversation

colesbury
Copy link
Contributor

Description

The "is_initialized_" flags is not protected by the GIL in free-threaded Python, so it needs to be an atomic field.

Fixes #5245

Suggested changelog entry:

Make ``gil_safe_call_once_and_store`` thread-safe in free-threaded CPython.

colesbury and others added 2 commits July 16, 2024 16:04
The "is_initialized_" flags is not protected by the GIL in free-threaded
Python, so it needs to be an atomic field.

Fixes pybind#5245
@@ -7,6 +7,9 @@

#include <cassert>
#include <mutex>
#ifdef Py_GIL_DISABLED
Copy link
Collaborator

Choose a reason for hiding this comment

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

Slight preference for an empty line before the #ifdef.

include/pybind11/gil_safe_call_once.h Show resolved Hide resolved
@rwgk rwgk merged commit 43de801 into pybind:master Jul 16, 2024
86 checks passed
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Jul 16, 2024
@colesbury colesbury deleted the issue5245-gil-safe-call-once branch July 16, 2024 18:07
@rwgk
Copy link
Collaborator

rwgk commented Aug 10, 2024

Hi @colesbury, considering that Python 3.13.0rc1 is out, we (me, @henryiii, @EthanSteinberg ) are thinking it may be useful to make another pybind11 release, with this PR included. We are wondering: Is there anything else obvious (from your vantage point) that we could/should do in pybind11, before making a release?

@colesbury
Copy link
Contributor Author

No, I can't think of anything else

@EthanSteinberg
Copy link
Collaborator

EthanSteinberg commented Aug 11, 2024

@colesbury I'm a bit worried about the logic in https://github.com/pybind/pybind11/blob/master/include/pybind11/detail/internals.h#L498 under free-threading and I was hoping I could get your help to double check that everything is thread safe.

internals is a global singleton struct shared across all pybind11 modules. get_internals() either retrieves that current global singleton or creates a new one.

I'm a bit worried about double initialization, where two copies of internals would be created by multiple modules being imported/initalized in different threads.

https://github.com/pybind/pybind11/blob/master/include/pybind11/detail/internals.h#L520-L524 are the key problematic lines where we retrieve the current global, and initialize it if necessary.

The problem is that if two threads hit those lines at the same time, both threads could see that internals is null and both threads could initialize internals, which would lead to two copies.

Currently this is protected via the GIL (https://github.com/pybind/pybind11/blob/master/include/pybind11/detail/internals.h#L505), but does that work with the new free threading code?

@colesbury
Copy link
Contributor Author

I think it would be better if we made the initialization in get_internals() explicitly thread-safe, but it's unlikely to cause problems because the free-threaded build temporarily enables the GIL when importing C extensions.

Do you want to create an issue for this?

@henryiii
Copy link
Collaborator

Another question (based on a question from @EthanSteinberg in separate communication): would it make sense for gil_scoped_acquire to take our lock? Code placed in a gil_scoped_acquire block expects to have a lock.

@colesbury
Copy link
Contributor Author

No, I think gil_scoped_acquire should just call PyGILState_Ensure or equivalent. If extensions want a global lock, they can skip setting py::mod_gil_not_used.

@EthanSteinberg
Copy link
Collaborator

@colesbury Thanks for confirming that the GIL is enabled during module initialization, so this is probably not a concern.

I don't know if it makes sense to make an issue now for this. Instead, let me write a PR next weekend that just adds some comments to the code pointing out why the module initialization GIL behavior allows that code to be safe even with free threading.

@colesbury
Copy link
Contributor Author

I'd rather not rely on the GIL behavior during module initialization. Additionally, at least in theory, I think get_internals() could be called for the first time after the C API extension was initialized.

@rwgk
Copy link
Collaborator

rwgk commented Aug 12, 2024

I'd rather not rely on the GIL behavior during module initialization. Additionally, at least in theory, I think get_internals() could be called for the first time after the C API extension was initialized.

I'm not sure if it's helpful, but JIC, get_internals() is called from here (PYBIND11_MODULE() macro):

@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Aug 13, 2024
henryiii pushed a commit that referenced this pull request Aug 13, 2024
)

* fix: Make gil_safe_call_once thread-safe in free-threaded CPython

The "is_initialized_" flags is not protected by the GIL in free-threaded
Python, so it needs to be an atomic field.

Fixes #5245

* style: pre-commit fixes

* Apply changes from code review

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@EthanSteinberg
Copy link
Collaborator

@colesbury @rwgk I created an issue to track / solve this #5316

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG]: gil_safe_call_once_and_store is not NoGil thread safe.
4 participants