Skip to content

Commit 91316e8

Browse files
committed
Add a test for per-interpreter GIL
Uses two extra threads to demonstrate that neither shares a GIL.
1 parent 77af339 commit 91316e8

File tree

3 files changed

+212
-2
lines changed

3 files changed

+212
-2
lines changed

include/pybind11/detail/common.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@
292292
#endif
293293

294294
// Slightly faster code paths are available when this is NOT defined, so undefine it for impls
295-
// that do not have subinterpreter. Nothing breaks if this is defined but the impl does not
295+
// that do not have subinterpreters. Nothing breaks if this is defined but the impl does not
296296
// actually support subinterpreters.
297297
#if PY_VERSION_HEX >= 0x030C0000 && !defined(PYPY_VERSION) && !defined(GRAALVM_PYTHON)
298298
# define PYBIND11_SUBINTERPRETER_SUPPORT

tests/test_embed/external_module.cpp

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ namespace py = pybind11;
66
* modules aren't preserved over a finalize/initialize.
77
*/
88

9-
PYBIND11_MODULE(external_module, m, py::mod_gil_not_used(), py::mod_multi_interpreter_one_gil()) {
9+
PYBIND11_MODULE(external_module,
10+
m,
11+
py::mod_multi_interpreter_one_gil(),
12+
py::mod_gil_not_used(),
13+
py::mod_per_interpreter_gil()) {
1014
class A {
1115
public:
1216
explicit A(int value) : v{value} {};

tests/test_embed/test_interpreter.cpp

+206
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ TEST_CASE("Restart the interpreter") {
301301
py::module_::import("__main__").attr("internals_destroy_test")
302302
= py::capsule(&ran, [](void *ran) {
303303
py::detail::get_internals();
304+
REQUIRE(has_state_dict_internals_obj());
305+
REQUIRE(has_pybind11_internals_static());
304306
*static_cast<bool *>(ran) = true;
305307
});
306308
REQUIRE_FALSE(has_state_dict_internals_obj());
@@ -384,6 +386,210 @@ TEST_CASE("Subinterpreter") {
384386
REQUIRE(has_state_dict_internals_obj());
385387
}
386388

389+
#if defined(PYBIND11_SUBINTERPRETER_SUPPORT)
390+
TEST_CASE("Multiple Subinterpreters") {
391+
// Make sure the module is in the main interpreter and save its pointer
392+
auto *main_ext = py::module_::import("external_module").ptr();
393+
auto main_int
394+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
395+
py::module_::import("external_module").attr("multi_interp") = "1";
396+
397+
auto *main_tstate = PyThreadState_Get();
398+
399+
/// Create and switch to a subinterpreter.
400+
auto *sub1_tstate = Py_NewInterpreter();
401+
py::detail::get_interpreter_count()++;
402+
403+
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
404+
405+
// The subinterpreter has its own copy of this module which is completely separate from main
406+
auto *sub1_ext = py::module_::import("external_module").ptr();
407+
REQUIRE(sub1_ext != main_ext);
408+
REQUIRE_FALSE(py::hasattr(py::module_::import("external_module"), "multi_interp"));
409+
py::module_::import("external_module").attr("multi_interp") = "2";
410+
// The sub-interpreter also has its own internals
411+
auto sub1_int
412+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
413+
REQUIRE(sub1_int != main_int);
414+
415+
// Create another interpreter
416+
auto *sub2_tstate = Py_NewInterpreter();
417+
py::detail::get_interpreter_count()++;
418+
419+
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
420+
421+
// The second subinterpreter is separate from both main and the other sub-interpreter
422+
auto *sub2_ext = py::module_::import("external_module").ptr();
423+
REQUIRE(sub2_ext != main_ext);
424+
REQUIRE(sub2_ext != sub1_ext);
425+
REQUIRE_FALSE(py::hasattr(py::module_::import("external_module"), "multi_interp"));
426+
py::module_::import("external_module").attr("multi_interp") = "3";
427+
// The sub-interpreter also has its own internals
428+
auto sub2_int
429+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
430+
REQUIRE(sub2_int != main_int);
431+
REQUIRE(sub2_int != sub1_int);
432+
433+
PyThreadState_Swap(sub1_tstate); // go back to sub1
434+
435+
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
436+
== "2");
437+
438+
PyThreadState_Swap(main_tstate); // go back to main
439+
440+
auto post_int
441+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
442+
// Make sure internals went back the way it was before
443+
REQUIRE(main_int == post_int);
444+
445+
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
446+
== "1");
447+
448+
PyThreadState_Swap(sub1_tstate);
449+
Py_EndInterpreter(sub1_tstate);
450+
PyThreadState_Swap(sub2_tstate);
451+
Py_EndInterpreter(sub2_tstate);
452+
453+
py::detail::get_interpreter_count() = 1;
454+
PyThreadState_Swap(main_tstate);
455+
}
456+
#endif
457+
458+
#if defined(Py_MOD_PER_INTERPRETER_GIL_SUPPORTED) && defined(PYBIND11_SUBINTERPRETER_SUPPORT)
459+
TEST_CASE("Per-Subinterpreter GIL") {
460+
auto main_int
461+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
462+
463+
std::atomic<int> started = 0, finished = 0, failure = 0, sync = 0;
464+
465+
// REQUIRE throws on failure, so we can't use it within the thread
466+
# define T_REQUIRE(status) \
467+
do { \
468+
assert(status); \
469+
if (!(status)) \
470+
++failure; \
471+
} while (0)
472+
473+
auto &&thread_main = [&](int num) {
474+
while (started == 0)
475+
std::this_thread::sleep_for(std::chrono::microseconds(1));
476+
++started;
477+
478+
py::gil_scoped_acquire gil;
479+
auto main_tstate = PyThreadState_Get();
480+
481+
// we have the GIL, we can access the main interpreter
482+
auto t_int
483+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
484+
T_REQUIRE(t_int == main_int);
485+
py::module_::import("external_module").attr("multi_interp") = "1";
486+
487+
PyInterpreterConfig cfg = _PyInterpreterConfig_INIT;
488+
PyThreadState *sub = nullptr;
489+
auto status = Py_NewInterpreterFromConfig(&sub, &cfg);
490+
T_REQUIRE(!PyStatus_IsError(status));
491+
492+
py::detail::get_interpreter_count()++;
493+
494+
py::list(py::module_::import("sys").attr("path")).append(py::str("."));
495+
496+
// we have switched to the new interpreter and released the main gil
497+
498+
// widget_module did not provide the mod_per_interpreter_gil tag, so it cannot be imported
499+
bool caught = false;
500+
try {
501+
py::module_::import("widget_module");
502+
} catch (pybind11::error_already_set &pe) {
503+
T_REQUIRE(pe.matches(PyExc_ImportError));
504+
std::string msg(pe.what());
505+
T_REQUIRE(msg.find("does not support loading in subinterpreters")
506+
!= std::string::npos);
507+
caught = true;
508+
}
509+
T_REQUIRE(caught);
510+
511+
T_REQUIRE(!py::hasattr(py::module_::import("external_module"), "multi_interp"));
512+
py::module_::import("external_module").attr("multi_interp") = std::to_string(num);
513+
514+
// wait for something to set sync to our thread number
515+
// we are holding our subinterpreter's GIL
516+
while (sync != num)
517+
std::this_thread::sleep_for(std::chrono::microseconds(1));
518+
519+
// now change it so the next thread can mvoe on
520+
++sync;
521+
522+
// but keep holding the GIL until after the next thread moves on as well
523+
while (sync == num + 1)
524+
std::this_thread::sleep_for(std::chrono::microseconds(1));
525+
526+
// one last check before quitting the thread, the internals should be different
527+
auto sub_int
528+
= py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>();
529+
T_REQUIRE(sub_int != main_int);
530+
531+
Py_EndInterpreter(sub);
532+
533+
++finished;
534+
535+
PyThreadState_Swap(
536+
main_tstate); // switch back so the scoped_acquire can release the GIL properly
537+
};
538+
539+
std::thread(thread_main, 1).detach();
540+
std::thread(thread_main, 2).detach();
541+
542+
// we spawned two threads, at this point they are both waiting for started to increase
543+
++started;
544+
545+
// ok now wait for the threads to start
546+
while (started != 3)
547+
std::this_thread::sleep_for(std::chrono::microseconds(1));
548+
549+
// we still hold the main GIL, at this point both threads are waiting on the main GIL
550+
// IN THE CASE of free threading, the threads are waiting on sync (because there is no GIL)
551+
552+
// IF the below code hangs in one of the wait loops, then the child thread GIL behavior did not
553+
// function as expected.
554+
{
555+
// release the GIL and allow the threads to run
556+
py::gil_scoped_release nogil;
557+
558+
// the threads are now waiting on the sync
559+
REQUIRE(sync == 0);
560+
561+
// this will trigger thread 1 and then advance and trigger 2 and then advance
562+
sync = 1;
563+
564+
// wait for thread 2 to advance
565+
while (sync != 3)
566+
std::this_thread::sleep_for(std::chrono::microseconds(1));
567+
568+
// we know now that thread 1 has run and may be finishing
569+
// and thread 2 is waiting for permission to advance
570+
571+
// so we move sync so that thread 2 can finish executing
572+
++sync;
573+
574+
++finished;
575+
576+
// now wait for both threads to complete
577+
while (finished != 3)
578+
std::this_thread::sleep_for(std::chrono::microseconds(1));
579+
}
580+
581+
// now we have the gil again, sanity check
582+
REQUIRE(py::cast<std::string>(py::module_::import("external_module").attr("multi_interp"))
583+
== "1");
584+
585+
// the threads are stopped. we can now lower this for the rest of the test
586+
py::detail::get_interpreter_count() = 1;
587+
588+
// make sure nothing unexpected happened inside the threads, now that they are completed
589+
REQUIRE(failure == 0);
590+
}
591+
#endif
592+
387593
TEST_CASE("Execution frame") {
388594
// When the interpreter is embedded, there is no execution frame, but `py::exec`
389595
// should still function by using reasonable globals: `__main__.__dict__`.

0 commit comments

Comments
 (0)