Skip to content

Commit 58ebbf0

Browse files
committed
Add test for multi-threaded import of multi-phase module
Also tests module state isolation
1 parent c86ea20 commit 58ebbf0

2 files changed

Lines changed: 182 additions & 4 deletions

File tree

test/fabscript

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ tests.append(extension_test('shared_ptr',
124124
#tests.append(extension_test('auto_ptr',
125125
# condition=set.define.contains('HAS_CXX11')))
126126

127-
import_ = binary('import_', ['import_.cpp', src.bpl], features=features|python_libs)
128127
if platform.os == 'Windows':
129-
command = """set PATH=$(runpath);%PATH%
128+
command2arg = """set PATH=$(runpath);%PATH%
130129
$(>[0]) $(>[1])"""
131130
else:
132-
command = 'LD_LIBRARY_PATH=$(runpath) $(>[0]) $(>[1])'
131+
command2arg = 'LD_LIBRARY_PATH=$(runpath) $(>[0]) $(>[1])'
133132

133+
import_ = binary('import_', ['import_.cpp', src.bpl], features=features|python_libs)
134134
tests.append(test('import', [import_, 'import_.py'],
135-
run=action('run', command),
135+
run=action('run', command2arg),
136136
features=features))
137137

138138
tests.append(extension_test('calling_conventions',
@@ -183,4 +183,17 @@ tests.append(extension_test("module_multi_phase_nogil",
183183
tests.append(extension_test("module_multi_phase_state",
184184
condition=python_version_major > 3 or (python_version_major == 3 and python_version_minor >= 5)))
185185

186+
187+
if platform.os == 'Windows':
188+
command1arg = """set PATH=$(runpath);%PATH%
189+
$(>[0])"""
190+
else:
191+
command1arg = 'LD_LIBRARY_PATH=$(runpath) $(>[0])'
192+
193+
multi_thread_import_ = binary('multi_thread_import_', ['multi_thread_import_.cpp', src.bpl], features=features|python_libs)
194+
tests.append(test('multi_thread_import', [multi_thread_import_],
195+
run=action('run', command1arg),
196+
features=features,
197+
condition=python_version_major > 3 or (python_version_major == 3 and python_version_minor >= 5)))
198+
186199
default = report('report', tests, fail_on_failures=True)

test/multi_thread_import_.cpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright Stefan Seefeld 2007.
2+
// Distributed under the Boost Software License, Version 1.0. (See
3+
// accompanying file LICENSE_1_0.txt or copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#include <boost/python.hpp>
7+
8+
#include <boost/detail/lightweight_test.hpp>
9+
#include <boost/bind/bind.hpp>
10+
#include <iostream>
11+
#include <thread>
12+
13+
namespace bpl = boost::python;
14+
15+
struct TestState
16+
{
17+
int x;
18+
};
19+
20+
int get_state()
21+
{
22+
TestState* state = reinterpret_cast<TestState*>(PyModule_GetState(bpl::import("multi_thread_import_").ptr()));
23+
if (state)
24+
{
25+
return state->x;
26+
}
27+
return -2;
28+
}
29+
30+
bool set_state(int value)
31+
{
32+
TestState* state = reinterpret_cast<TestState*>(PyModule_GetState(bpl::import("multi_thread_import_").ptr()));
33+
if (state)
34+
{
35+
state->x = value;
36+
return true;
37+
}
38+
return false;
39+
}
40+
41+
BOOST_PYTHON_MODULE_WITH_STATE(multi_thread_import_, TestState) {
42+
state->x = -1;
43+
bpl::def("get_state", get_state);
44+
bpl::def("set_state", set_state);
45+
}
46+
47+
void import_test(int thread_id)
48+
{
49+
// Retrieve the main module
50+
bpl::object import_ = bpl::import("multi_thread_import_");
51+
bool result = bpl::extract<bool>(import_.attr("set_state")(thread_id)) BOOST_EXTRACT_WORKAROUND;
52+
BOOST_TEST(result);
53+
54+
PyThreadState* _save = PyEval_SaveThread();
55+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
56+
PyEval_RestoreThread(_save);
57+
58+
import_ = bpl::import("multi_thread_import_");
59+
int value = bpl::extract<int>(import_.attr("get_state")()) BOOST_EXTRACT_WORKAROUND;
60+
std::cout << "Thread " << thread_id << ": " << value << std::endl;
61+
BOOST_TEST(value == thread_id);
62+
}
63+
64+
void thread_func(int thread_id, PyInterpreterState* main_interp)
65+
{
66+
// Initialize thread state for main interpreter in sub thread
67+
PyThreadState* main_tstate = PyThreadState_New(main_interp);
68+
69+
if (!main_tstate) {
70+
BOOST_ERROR("Failed to create main thread state");
71+
return;
72+
}
73+
74+
// Lock GIL before create sub interpreter
75+
PyEval_AcquireThread(main_tstate);
76+
77+
// Create sub interpreter
78+
PyThreadState* sub_thread_state = Py_NewInterpreter();
79+
80+
if (!sub_thread_state) {
81+
BOOST_ERROR("Failed to create sub-interpreter");
82+
// Release GIL
83+
PyEval_ReleaseThread(main_tstate);
84+
// Delete sub thread state
85+
PyThreadState_Delete(main_tstate);
86+
return;
87+
}
88+
89+
if (bpl::handle_exception(boost::bind(import_test, thread_id)))
90+
{
91+
if (PyErr_Occurred())
92+
{
93+
BOOST_ERROR("Python Error detected");
94+
PyErr_Print();
95+
}
96+
else
97+
{
98+
BOOST_ERROR("A C++ exception was thrown for which "
99+
"there was no exception handler registered.");
100+
}
101+
}
102+
103+
// Finish sub interpreter
104+
Py_EndInterpreter(sub_thread_state);
105+
106+
// Lock GIL again after for main interpreter
107+
PyEval_AcquireThread(main_tstate);
108+
// Release GIL
109+
PyEval_ReleaseThread(main_tstate);
110+
// Delete sub thread state
111+
PyThreadState_Delete(main_tstate);
112+
}
113+
114+
int main(int argc, char **argv)
115+
{
116+
BOOST_TEST(argc == 1);
117+
118+
// Register the module with the interpreter
119+
if (PyImport_AppendInittab(const_cast<char*>("multi_thread_import_"),
120+
#if PY_VERSION_HEX >= 0x03000000
121+
PyInit_multi_thread_import_
122+
#else
123+
multi_thread_import_
124+
#endif
125+
) == -1)
126+
{
127+
BOOST_ERROR("Failed to add import2_ to the interpreter's "
128+
"builtin modules");
129+
}
130+
131+
// Initialize the interpreter
132+
Py_Initialize();
133+
134+
// Capture main interpreter state to pass it to sub threads
135+
PyInterpreterState* main_interp = PyThreadState_Get()->interp;
136+
137+
const int num_threads = 5;
138+
std::vector<std::thread> threads;
139+
140+
// Release the main thread's GIL
141+
PyThreadState* main_thread_state = PyEval_SaveThread();
142+
143+
for (int i = 0; i < num_threads; ++i)
144+
{
145+
threads.emplace_back(thread_func, i, main_interp);
146+
}
147+
148+
for (auto& t : threads)
149+
{
150+
t.join();
151+
}
152+
153+
// Restore the main thread's GIL
154+
PyEval_RestoreThread(main_thread_state);
155+
156+
// Boost.Python doesn't support Py_Finalize yet.
157+
// Py_Finalize();
158+
return boost::report_errors();
159+
}
160+
161+
// Including this file makes sure
162+
// that on Windows, any crashes (e.g. null pointer dereferences) invoke
163+
// the debugger immediately, rather than being translated into structured
164+
// exceptions that can interfere with debugging.
165+
#include "module_tail.cpp"

0 commit comments

Comments
 (0)