blob: f64a1668503c8afc2bb202cd2c51cb521c52f27c [file]
/*
tests/test_thread.cpp -- call pybind11 bound methods in threads
Copyright (c) 2021 Laramie Leavitt (Google LLC) <lar@google.com>
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/
#include <pybind11/cast.h>
#include <pybind11/detail/internals.h>
#include <pybind11/pybind11.h>
#include "pybind11_tests.h"
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#if defined(PYBIND11_HAS_STD_BARRIER)
# include <barrier>
#endif
namespace py = pybind11;
namespace {
struct IntStruct {
explicit IntStruct(int v) : value(v) {};
~IntStruct() { value = -value; }
IntStruct(const IntStruct &) = default;
IntStruct &operator=(const IntStruct &) = default;
int value;
};
struct EmptyStruct {};
EmptyStruct SharedInstance;
#ifdef Py_GIL_DISABLED
// Holds detail::internals::mutex on a native thread that never touches the Python C API, so that
// a Python-level call can be checked for independence from the internals lock. The lock is a
// no-op unless Py_GIL_DISABLED, hence the #ifdef. See test_dispatch_does_not_need_internals_lock.
class internals_lock_holder {
public:
internals_lock_holder() = default;
internals_lock_holder(const internals_lock_holder &) = delete;
internals_lock_holder &operator=(const internals_lock_holder &) = delete;
~internals_lock_holder() {
if (thread.joinable()) {
request_release_and_join();
}
}
// Returns once the native thread holds the internals mutex. The thread releases it when
// release_and_join() is called, or after `watchdog` if nobody asks: that keeps the process
// moving when the call under test blocks on the mutex.
void start(py::detail::pymutex &internals_mutex, std::chrono::milliseconds watchdog) {
if (thread.joinable()) {
throw std::runtime_error("internals_lock_holder is already started");
}
held = false;
release_requested = false;
released_by_watchdog = false;
thread = std::thread([this, &internals_mutex, watchdog]() {
internals_mutex.lock();
{
std::lock_guard<std::mutex> lock(m);
held = true;
}
cv.notify_all();
{
std::unique_lock<std::mutex> lock(m);
released_by_watchdog
= !cv.wait_for(lock, watchdog, [this]() { return release_requested; });
}
internals_mutex.unlock();
});
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [this]() { return held; });
}
// Returns true if the watchdog had already released the mutex.
bool release_and_join() {
if (!thread.joinable()) {
throw std::runtime_error("internals_lock_holder is not started");
}
return request_release_and_join();
}
private:
bool request_release_and_join() {
{
std::lock_guard<std::mutex> lock(m);
release_requested = true;
}
cv.notify_all();
thread.join();
return released_by_watchdog;
}
std::mutex m;
std::condition_variable cv;
bool held = false;
bool release_requested = false;
bool released_by_watchdog = false;
std::thread thread;
};
internals_lock_holder &get_internals_lock_holder() {
static internals_lock_holder holder;
return holder;
}
// Deliberately raw PyCFunctions, not pybind11 bindings: these must be callable while the
// internals mutex is held, and whether a pybind11 dispatch can be is exactly what the test checks.
PyObject *start_internals_lock_holder(PyObject *, PyObject *watchdog_seconds_obj) {
const double watchdog_seconds = PyFloat_AsDouble(watchdog_seconds_obj);
if (watchdog_seconds == -1.0 && PyErr_Occurred() != nullptr) {
return nullptr;
}
const auto watchdog = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::duration<double>(watchdog_seconds));
// Looked up while attached; the holder thread itself never uses the Python C API.
auto &internals_mutex = py::detail::get_internals().mutex;
std::string error;
PyThreadState *save = PyEval_SaveThread();
try {
get_internals_lock_holder().start(internals_mutex, watchdog);
} catch (const std::exception &e) {
error = e.what();
}
PyEval_RestoreThread(save);
if (!error.empty()) {
PyErr_SetString(PyExc_RuntimeError, error.c_str());
return nullptr;
}
Py_RETURN_NONE;
}
PyObject *release_internals_lock_holder(PyObject *, PyObject *) {
bool released_by_watchdog = false;
std::string error;
PyThreadState *save = PyEval_SaveThread();
try {
released_by_watchdog = get_internals_lock_holder().release_and_join();
} catch (const std::exception &e) {
error = e.what();
}
PyEval_RestoreThread(save);
if (!error.empty()) {
PyErr_SetString(PyExc_RuntimeError, error.c_str());
return nullptr;
}
return PyBool_FromLong(released_by_watchdog ? 1 : 0);
}
// Positive control for the test: a call that is known to take the internals lock.
PyObject *take_internals_lock(PyObject *, PyObject *) {
try {
py::detail::with_internals([](py::detail::internals &) {});
} catch (py::error_already_set &e) {
e.restore();
return nullptr;
}
Py_RETURN_NONE;
}
PyMethodDef internals_lock_methods[]
= {{"start_internals_lock_holder", start_internals_lock_holder, METH_O, nullptr},
{"release_internals_lock_holder", release_internals_lock_holder, METH_NOARGS, nullptr},
{"take_internals_lock", take_internals_lock, METH_NOARGS, nullptr},
{nullptr, nullptr, 0, nullptr}};
#endif // Py_GIL_DISABLED
} // namespace
TEST_SUBMODULE(thread, m) {
py::class_<IntStruct>(m, "IntStruct").def(py::init([](const int i) { return IntStruct(i); }));
// implicitly_convertible uses loader_life_support when an implicit
// conversion is required in order to lifetime extend the reference.
//
// This test should be run with ASAN for better effectiveness.
py::implicitly_convertible<int, IntStruct>();
m.def("test", [](int expected, const IntStruct &in) {
{
py::gil_scoped_release release;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (in.value != expected) {
throw std::runtime_error("Value changed!!");
}
});
m.def(
"test_no_gil",
[](int expected, const IntStruct &in) {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
if (in.value != expected) {
throw std::runtime_error("Value changed!!");
}
},
py::call_guard<py::gil_scoped_release>());
py::class_<EmptyStruct>(m, "EmptyStruct")
.def_readonly_static("SharedInstance", &SharedInstance);
#if defined(PYBIND11_HAS_STD_BARRIER)
// In the free-threaded build, during PyThreadState_Clear, removing the thread from the biased
// reference counting table may call destructors. Make sure that it doesn't crash.
m.def("test_pythread_state_clear_destructor", [](py::type cls) {
py::handle obj;
std::barrier barrier{2};
std::thread thread1{[&]() {
py::gil_scoped_acquire gil;
obj = cls().release();
barrier.arrive_and_wait();
}};
std::thread thread2{[&]() {
py::gil_scoped_acquire gil;
barrier.arrive_and_wait();
// ob_ref_shared becomes negative; transition to the queued state
obj.dec_ref();
}};
// jthread is not supported by Apple Clang
thread1.join();
thread2.join();
});
#endif
m.attr("defined_PYBIND11_HAS_STD_BARRIER") =
#ifdef PYBIND11_HAS_STD_BARRIER
true;
#else
false;
#endif
m.def("acquire_gil", []() { py::gil_scoped_acquire gil_acquired; });
// The call under test for test_dispatch_does_not_need_internals_lock: the smallest possible
// bound function, so that the only pybind11 machinery involved is cpp_function::dispatcher().
m.def("dispatch_noop", []() {});
#ifdef Py_GIL_DISABLED
if (PyModule_AddFunctions(m.ptr(), internals_lock_methods) != 0) {
throw py::error_already_set();
}
#endif
// NOTE: std::string_view also uses loader_life_support to ensure that
// the string contents remain alive, but that's a C++ 17 feature.
}