Skip to content

Commit 7daecd7

Browse files
committed
Reorg code to avoid duplicates
1 parent 99a095d commit 7daecd7

File tree

2 files changed

+51
-61
lines changed

2 files changed

+51
-61
lines changed

include/pybind11/gil_safe_call_once.h

Lines changed: 44 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ class gil_safe_call_once_and_store {
181181
// There can be multiple threads going through here.
182182
storage_type *value = nullptr;
183183
{
184-
gil_scoped_acquire gil_acq;
185-
// Only one thread will enter here at a time.
184+
gil_scoped_acquire gil_acq; // Restore lock ordering.
185+
// This function is thread-safe under free-threading.
186186
value = get_or_create_storage_in_state_dict();
187187
}
188188
assert(value != nullptr);
@@ -259,72 +259,56 @@ class gil_safe_call_once_and_store {
259259
// First, try to get existing storage (fast path).
260260
{
261261
capsule_obj = detail::dict_getitemstring(state_dict.ptr(), key.c_str());
262-
if (capsule_obj != nullptr) {
263-
// Storage already exists, get the storage pointer from the existing capsule.
264-
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
265-
if (!raw_ptr) {
266-
raise_from(PyExc_SystemError,
267-
"pybind11::gil_safe_call_once_and_store::"
268-
"get_or_create_storage_in_state_dict() FAILED "
269-
"(get existing)");
270-
throw error_already_set();
271-
}
272-
return static_cast<storage_type *>(raw_ptr);
273-
}
274-
if (PyErr_Occurred()) {
262+
if (capsule_obj == nullptr && PyErr_Occurred()) {
275263
throw error_already_set();
276264
}
265+
// Fallthrough if capsule_obj is nullptr (not found).
266+
// Otherwise, we have found the existing storage (most common case) and return it
267+
// below.
277268
}
278269

279-
// Storage doesn't exist yet, create a new one。
280-
// Use unique_ptr for exception safety: if capsule creation throws,
281-
// the storage is automatically deleted.
282-
auto storage_ptr = std::unique_ptr<storage_type>(new storage_type{});
283-
// Create capsule with destructor to clean up when the interpreter shuts down.
284-
auto new_capsule = capsule(
285-
storage_ptr.get(), [](void *ptr) -> void { delete static_cast<storage_type *>(ptr); });
286-
287-
// Use `PyDict_SetDefault` for atomic test-and-set:
288-
// - If key doesn't exist, inserts our capsule and returns it.
289-
// - If key exists (another thread inserted first), returns the existing value.
290-
// This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict.
291-
//
292-
// NOTE: Here we use `dict_setdefaultstring` instead of `dict_setdefaultstringref` because
293-
// the capsule is kept alive until interpreter shutdown, so we do not need to handle incref
294-
// and decref here.
295-
capsule_obj
296-
= detail::dict_setdefaultstring(state_dict.ptr(), key.c_str(), new_capsule.ptr());
297270
if (capsule_obj == nullptr) {
298-
throw error_already_set();
299-
}
300-
301-
// Check whether we inserted our new capsule or another thread did.
302-
if (capsule_obj == new_capsule.ptr()) {
303-
// We successfully inserted our new capsule, release ownership from unique_ptr.
304-
return storage_ptr.release();
305-
}
306-
// Another thread already inserted a capsule, use theirs and discard ours.
307-
{
308-
// Disable the destructor of our unused capsule to prevent double-free:
309-
// unique_ptr will clean up the storage on function exit, and the capsule should not.
310-
if (PyCapsule_SetDestructor(new_capsule.ptr(), nullptr) != 0) {
311-
raise_from(PyExc_SystemError,
312-
"pybind11::gil_safe_call_once_and_store::"
313-
"get_or_create_storage_in_state_dict() FAILED "
314-
"(clear destructor of unused capsule)");
315-
throw error_already_set();
316-
}
317-
// Get the storage pointer from the existing capsule.
318-
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
319-
if (!raw_ptr) {
320-
raise_from(PyExc_SystemError,
321-
"pybind11::gil_safe_call_once_and_store::"
322-
"get_or_create_storage_in_state_dict() FAILED "
323-
"(get after setdefault)");
271+
// Storage doesn't exist yet, create a new one.
272+
// Use unique_ptr for exception safety: if capsule creation throws, the storage is
273+
// automatically deleted.
274+
auto storage_ptr = std::unique_ptr<storage_type>(new storage_type{});
275+
// Create capsule with destructor to clean up when the interpreter shuts down.
276+
auto new_capsule = capsule(storage_ptr.get(), [](void *ptr) -> void {
277+
delete static_cast<storage_type *>(ptr);
278+
});
279+
// At this point, the capsule object is created successfully.
280+
// Release the unique_ptr and let the capsule object own the storage to avoid
281+
// double-free.
282+
storage_ptr.reset();
283+
284+
// Use `PyDict_SetDefault` for atomic test-and-set:
285+
// - If key doesn't exist, inserts our capsule and returns it.
286+
// - If key exists (another thread inserted first), returns the existing value.
287+
// This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict.
288+
//
289+
// NOTE: Here we use `dict_setdefaultstring` instead of `dict_setdefaultstringref`
290+
// because the capsule is kept alive until interpreter shutdown, so we do not need to
291+
// handle incref and decref here.
292+
capsule_obj
293+
= detail::dict_setdefaultstring(state_dict.ptr(), key.c_str(), new_capsule.ptr());
294+
if (capsule_obj == nullptr) {
324295
throw error_already_set();
325296
}
326-
return static_cast<storage_type *>(raw_ptr);
297+
// - If key already existed, our `new_capsule` is not inserted, it will be destructed
298+
// when going out of scope here, which will also free the storage.
299+
// - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the
300+
// state dict will incref it.
301+
}
302+
303+
// Get the storage pointer from the capsule.
304+
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
305+
if (!raw_ptr) {
306+
raise_from(PyExc_SystemError,
307+
"pybind11::gil_safe_call_once_and_store::"
308+
"get_or_create_storage_in_state_dict() FAILED");
309+
throw error_already_set();
327310
}
311+
return static_cast<storage_type *>(raw_ptr);
328312
}
329313

330314
// No storage needed when subinterpreter support is enabled.

tests/test_multiple_interpreters/test_multiple_interpreters.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,13 @@ def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
264264
)
265265
except subprocess.CalledProcessError as ex:
266266
pytest.fail(
267-
f"Subprocess failed with exit code {ex.returncode}.\nOutput:\n{ex.output}"
267+
f"Subprocess failed with exit code {ex.returncode}.\n\n"
268+
f"Code:\n"
269+
f"```python\n"
270+
f"{code}\n"
271+
f"```\n\n"
272+
f"Output:\n"
273+
f"{ex.output}"
268274
)
269275

270276

0 commit comments

Comments
 (0)