[C++-sig] Threads and Boost.Python
Patrick Hartling
patrick at vrac.iastate.edu
Fri Apr 18 19:08:26 CEST 2003
I was able to sort out the multi-threading issues I was having earlier
this week with Boost.Python thanks to David Abrahams' suggestions. What
I came up with is a relatively simple guard object that is instantiated
whenever C++ code calls into the Python interpreter. To make this guard
work, I had to deal with two key issues:
1. Ensuring that threads created by C/C++ bootstrapped themselves with
the Python interpreter.
2. Preventing deadlocks if the same thread tried to acquire the GIL
more than once.
Issue #1 was straightforward enough to handle. The C++ code base that I
am exposing to Python includes a cross-platform data structure for
thread-specific data (TSD). Within the guard, there is a static data
member that is an instance of this TSD structure. Our TSD handler is a
templated type, and the struct I gave it contained two fields:
struct State
{
State() : isLocked(false), pyState(NULL)
{}
bool isLocked;
PyThreadState* pyState;
};
The boolean field is used to deal with Issue #2 from above; the thread
state object needs to be different for each unique thread calling into
the Python interpreter. The nice thing about our TSD handler is that a
new instance of the above structure will be instantiated automatically
for each thread, so whenever a new thread creates an instance of the
guard, the guard constructor handles filling in the State instance
fields. The code from my guard constructor is this:
Guard::Guard() : mMyLock(false)
{
if ( NULL == mState->pyState )
{
mState->pyState = PyThreadState_New(PyInterpreterState_New());
}
if ( ! mState->isLocked )
{
// Acquire the GIL.
PyEval_AcquireThread(mState->pyState);
mState->isLocked = true;
mMyLock = true;
}
}
Here, Guard::mState is the static data member that is an instance of our
TSD handler.
The destructor is this:
Guard::~Guard()
{
if ( mMyLock && mState->isLocked )
{
// Release the GIL.
PyEval_ReleaseThread(mState->pyState);
mState->isLocked = false;
}
}
The last detail is the data member Guard::mMyLock. That is a boolean to
handle keep track of which guard instance actually acquired the GIL so
that it is the one to release it upon destruction.
Dealing with the recursive lock problem was probably the trickiest part.
It's too bad that the GIL cannot be locked twice by the same thread,
but perhaps that behavior doesn't exist for all the threading
implementations upon which the Python interpreter runs.
On the whole, this guard concept is working very well for me, though I
haven't even been using it for 48 hours yet. Conceptually, I think
something like a "synchronized call" (to steal a Java concept) in
Boost.Python would help automate this process. At this time, however, I
don't know how well my guard will apply to arbitrary calls into the
Python interpreter from a thread. It works like a charm for
boost::python::call_method<>(), and that's what I care about the most
right now.
Two side issues I ran into involved deciding when to call
PyEval_InitThreads() and where to put the two macros for allowing and
disallowing threads. With the way our software works, the main thread
basically doesn't do anything once it starts a microkernel's control
thread. The primordial thread just sits and blocks on a semaphore
waiting for the microkernel to shut down. From there on, all calls into
the Python interpreter from C++ are guaranteed to come from a thread
other than the one that started the interpreter. So, I put
Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS in a wrapper function
that is exposed to Python where the Python "main" function makes the
sempahore wait call. There will be other cases I'll have to hunt down as
we expand our use of Boost.Python, but at least I have the general idea
of what to watch out for.
-Patrick
--
Patrick L. Hartling | Research Assistant, VRAC
patrick at vrac.iastate.edu | 2624 Howe Hall: 1.515.294.4916
http://www.137.org/patrick/ | http://www.vrac.iastate.edu/
More information about the Cplusplus-sig
mailing list