Boost.Python: Wrap Functions to Release the Gil

Boost.Python: Wrap functions to release the GIL

Exposing functors as methods is not officially supported. The supported approach would be to expose a non-member function that delegates to the member-function. However, this can result in a large amount of boilerplate code.

As best as I can tell, Boost.Python's implementation does not explicitly preclude functors, as it allows for instances of python::object to be exposed as a method. However, Boost.Python does place some requirements on the type of object being exposed as a method:

  • The functor is CopyConstructible.
  • The functor is callable. I.e. instance o can be called o(a1, a2, a3).
  • The call signature must be available as meta-data during runtime. Boost.Python calls the boost::python::detail::get_signature() function to obtain this meta-data. The meta-data is used internally to setup proper invocation, as well as for dispatching from Python to C++.

The latter requirement is where it gets complex. For some reason that is not immediately clear to me, Boost.Python invokes get_signature() through a qualified-id, preventing argument dependent lookup. Therefore, all candidates for get_signature() must be declared before the calling template's definition context. For example, the only overloads for get_signature() that are considered are those declared before the definition of templates that invoke it, such as class_, def(), and make_function(). To account for this behavior, when enabling a functor in Boost.Python, one must provide a get_signature() overload prior to including Boost.Python or explicitly provide a meta-sequence representing the signature to make_function().


Lets work through some examples of enabling functor support, as well as providing functors that support guards. I have opted to not use C++11 features. As such, there will be some boilerplate code that could be reduced with variadic templates. Additionally, all of the examples will use the same model that provides two non-member functions and a spam class that has two member-functions:

/// @brief Mockup class with member functions.
class spam
{
public:
void action()
{
std::cout << "spam::action()" << std::endl;
}

int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};

// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}

int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}

Enabling boost::function

When using the preferred syntax for Boost.Function, decomposing the signature into meta-data that meets Boost.Python requirements can be done with Boost.FunctionTypes. Here is a complete example enabling boost::function functors to be exposed as a Boost.Python method:

#include <iostream>

#include <boost/function.hpp>
#include <boost/function_types/components.hpp>

namespace boost {
namespace python {
namespace detail {
// get_signature overloads must be declared before including
// boost/python.hpp. The declaration must be visible at the
// point of definition of various Boost.Python templates during
// the first phase of two phase lookup. Boost.Python invokes the
// get_signature function via qualified-id, thus ADL is disabled.

/// @brief Get the signature of a boost::function.
template <typename Signature>
inline typename boost::function_types::components<Signature>::type
get_signature(boost::function<Signature>&, void* = 0)
{
return typename boost::function_types::components<Signature>::type();
}

} // namespace detail
} // namespace python
} // namespace boost

#include <boost/python.hpp>

/// @brief Mockup class with member functions.
class spam
{
public:
void action()
{
std::cout << "spam::action()" << std::endl;
}

int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};

// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}

int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}

BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;

// Expose class and member-function.
python::class_<spam>("Spam")
.def("action", &spam::action)
.def("times_two", boost::function<int(spam&, int)>(
&spam::times_two))
;

// Expose non-member function.
python::def("action", &action);
python::def("times_two", boost::function<int()>(
boost::bind(×_two, 21)));
}

And its usage:

>>> import example
>>> spam = example.Spam()
>>> spam.action()
spam::action()
>>> spam.times_two(5)
spam::times_two()
10
>>> example.action()
action()
>>> example.times_two()
times_two()
42

When providing a functor that will invoke a member-function, the provided signature needs to be the non-member function equivalent. In this case, int(spam::*)(int) becomes int(spam&, int).

// ...
.def("times_two", boost::function<int(spam&, int)>(
&spam::times_two))
;

Also, arguments can be bound to the functors with boost::bind. For example, calling example.times_two() does not have to provide an argument, as 21 is already bound to the functor.

python::def("times_two", boost::function<int()>(
boost::bind(×_two, 21)));

Custom functor with guards

Expanding upon the above example, one can enable custom functor types to be used with Boost.Python. Lets create a functor, called guarded_function, that will use RAII, only invoking the wrapped function during the RAII object's lifetime.

/// @brief Functor that will invoke a function while holding a guard.
/// Upon returning from the function, the guard is released.
template <typename Signature,
typename Guard>
class guarded_function
{
public:

typedef typename boost::function_types::result_type<Signature>::type
result_type;

template <typename Fn>
guarded_function(Fn fn)
: fn_(fn)
{}

result_type operator()()
{
Guard g;
return fn_();
}

// ... overloads for operator()

private:
boost::function<Signature> fn_;
};

The guarded_function provides similar semantics to the Python with statement. Thus, to keep with the Boost.Python API name choices, a with() C++ function will provide a way to create functors.

/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn>
boost::python::object
with(Fn fn)
{
return boost::python::make_function(
guarded_function<Guard, Fn>(fn), ...);
}

This allows for functions to be exposed which will run with a guard in a non-intrusive manner:

class no_gil; // Guard

// ...
.def("times_two", with<no_gil>(&spam::times_two))
;

Additionally, the with() function provides the ability to deduce the function signatures, allowing the meta-data signature to be explicitly provided to Boost.Python rather than having to overload boost::python::detail::get_signature().

Here is the complete example, using two RAII types:

  • no_gil: Releases GIL in constructor, and reacquires GIL in destructor.
  • echo_guard: Prints in constructor and destructor.
#include <iostream>

#include <boost/function.hpp>
#include <boost/function_types/components.hpp>
#include <boost/function_types/function_type.hpp>
#include <boost/function_types/result_type.hpp>
#include <boost/python.hpp>
#include <boost/tuple/tuple.hpp>

namespace detail {

/// @brief Functor that will invoke a function while holding a guard.
/// Upon returning from the function, the guard is released.
template <typename Signature,
typename Guard>
class guarded_function
{
public:

typedef typename boost::function_types::result_type<Signature>::type
result_type;

template <typename Fn>
guarded_function(Fn fn)
: fn_(fn)
{}

result_type operator()()
{
Guard g;
return fn_();
}

template <typename A1>
result_type operator()(A1 a1)
{
Guard g;
return fn_(a1);
}

template <typename A1, typename A2>
result_type operator()(A1 a1, A2 a2)
{
Guard g;
return fn_(a1, a2);
}

private:
boost::function<Signature> fn_;
};

/// @brief Provides signature type.
template <typename Signature>
struct mpl_signature
{
typedef typename boost::function_types::components<Signature>::type type;
};

// Support boost::function.
template <typename Signature>
struct mpl_signature<boost::function<Signature> >:
public mpl_signature<Signature>
{};

/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn,
typename Policy>
boost::python::object with_aux(Fn fn, const Policy& policy)
{
// Obtain the components of the Fn. This will decompose non-member
// and member functions into an mpl sequence.
// R (*)(A1) => R, A1
// R (C::*)(A1) => R, C*, A1
typedef typename mpl_signature<Fn>::type mpl_signature_type;

// Synthesize the components into a function type. This process
// causes member functions to require the instance argument.
// This is necessary because member functions will be explicitly
// provided the 'self' argument.
// R, A1 => R (*)(A1)
// R, C*, A1 => R (*)(C*, A1)
typedef typename boost::function_types::function_type<
mpl_signature_type>::type signature_type;

// Create a callable boost::python::object that delegates to the
// guarded_function.
return boost::python::make_function(
guarded_function<signature_type, Guard>(fn),
policy, mpl_signature_type());
}

} // namespace detail

/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn,
typename Policy>
boost::python::object with(const Fn& fn, const Policy& policy)
{
return detail::with_aux<Guard>(fn, policy);
}

/// @brief Create a callable object with guards.
template <typename Guard,
typename Fn>
boost::python::object with(const Fn& fn)
{
return with<Guard>(fn, boost::python::default_call_policies());
}

/// @brief Mockup class with member functions.
class spam
{
public:
void action()
{
std::cout << "spam::action()" << std::endl;
}

int times_two(int x)
{
std::cout << "spam::times_two()" << std::endl;
return 2 * x;
}
};

// Mockup non-member functions.
void action()
{
std::cout << "action()" << std::endl;
}

int times_two(int x)
{
std::cout << "times_two()" << std::endl;
return 2 * x;
}

/// @brief Guard that will unlock the GIL upon construction, and
/// reacquire it upon destruction.
struct no_gil
{
public:
no_gil() { state_ = PyEval_SaveThread();
std::cout << "no_gil()" << std::endl; }
~no_gil() { std::cout << "~no_gil()" << std::endl;
PyEval_RestoreThread(state_); }
private:
PyThreadState* state_;
};

/// @brief Guard that prints to std::cout.
struct echo_guard
{
echo_guard() { std::cout << "echo_guard()" << std::endl; }
~echo_guard() { std::cout << "~echo_guard()" << std::endl; }
};

BOOST_PYTHON_MODULE(example)
{
namespace python = boost::python;

// Expose class and member-function.
python::class_<spam>("Spam")
.def("action", &spam::action)
.def("times_two", with<no_gil>(&spam::times_two))
;

// Expose non-member function.
python::def("action", &action);
python::def("times_two", with<boost::tuple<no_gil, echo_guard> >(
×_two));
}

And its usage:

>>> import example
>>> spam = example.Spam()
>>> spam.action()
spam::action()
>>> spam.times_two(5)
no_gil()
spam::times_two()
~no_gil()
10
>>> example.action()
action()
>>> example.times_two(21)
no_gil()
echo_guard()
times_two()
~echo_guard()
~no_gil()
42

Notice how multiple guards can be provided by using a container type, such as boost::tuple:

  python::def("times_two", with<boost::tuple<no_gil, echo_guard> >(
×_two));

When invoked in Python, example.times_two(21) produces the following output:

no_gil()
echo_guard()
times_two()
~echo_guard()
~no_gil()
42

boost.python not supporting parallelism?

What you are running into is the python Global Interpreter Lock. The GIL only allows one thread at a time to run in the python interpreter.

One of the advantages of Boost.Python is that you can release the GIL, do C++ stuff, and then take it back when you are done. This is also a responsibility however. Python normally releases the GIL at regular intervals, to give other threads a chance to run. If you are in C++, this is your job. If you go crunch numbers for 2 hours while holding the GIL, you will freeze the whole interpreter.

This can be easy to fix with a little reverse RAII:

class releaseGIL{
public:
inline releaseGIL(){
save_state = PyEval_SaveThread();
}

inline ~releaseGIL(){
PyEval_RestoreThread(save_state);
}
private:
PyThreadState *save_state;
};

Now you can change your code like so:

class Foo{
public:
Foo(){}
void run(){
{
releaseGIL unlock = releaseGIL();
int seconds = 2;
clock_t endwait;
endwait = clock () + seconds * CLOCKS_PER_SEC ;
while (clock() < endwait) {}
}
}
};

It is VERY important to note that you MUST NOT touch any python code, or python data or call in to the interpreter while not holding the GIL. This will cause your interpreter to crash.

It is also possible to go the other way. A thread not currently holding the GIL can acquire it, and make calls in to python. This can be a thread that released the GIL earlier, or one that started in c++ and never had the GIL. Here is the RAII class for that:

class AcquireGIL 
{
public:
inline AcquireGIL(){
state = PyGILState_Ensure();
}

inline ~AcquireGIL(){
PyGILState_Release(state);
}
private:
PyGILState_STATE state;
};

Usage is left as an exercise for the student.

Additional note (I always forget to mention this):

If you are going to be messing with the GIL in c++ your module definition needs to start with this code:

BOOST_PYTHON_MODULE(ModuleName)
{
PyEval_InitThreads();

...
}

Python, Threads, the GIL, and C++

I found a really obscure post on the mailing list that said to use
PyEval_InitThreads();
in BOOST_PYTHON_MODULE
and that actually seemed to stop the crashes.

Its still a crap shoot whether it the program reports all the messages it got or not. If i send 2000, most of the time it says it got 2000, but sometimes it reports significantly less.

I suspect this might be due to the threads accessing my counter at the same time, so I am answering this question because that is a different problem.

To fix just do

BOOST_PYTHON_MODULE(MyLib)
{
PyEval_InitThreads();
class_ stuff

PyGILState_Ensure after Py_Finalize?

API for interpreter initialization/finalization includes Py_IsInitialized; returns non-zero between Py_Initialize and Py_Finalize, and zero before Py_Initialize and after Py_Finalize.

You'd have to test whether race conditions could mess you up here; it's wholly possible you could attempt to acquire the GIL, another thread calls Py_Finalize, and Py_Finalize blows away the lock from under you. There are some notes about PyGILState_* APIs not handling the existence of multiple interpreters properly that may or may not apply to your scenario (or hint at a similar issue that might lead to the speculative race I mentioned).

True multithreading with boost.python

The answer is no, the GIL will never truly multi-thread unless the DLL manually releases the lock. Python allows exactly one thread to run at a time unless the extension manually says, "I'm blocked, carry on without me." This is commonly done with the Py_BEGIN_ALLOW_THREADS macro (and undone with Py_END_ALLOW_THREADS) defined in python's include/ceval.h. Once an extension does this, python will allow another thread to run, and the first thread doing any python stuff will likely cause problems (as the comment question notes.) It's really meant for blocking on I/O or going into heavy compute time.

How to write a wrapper over functions and member functions that executes some code before and after the wrapped function?

In this case, you can write a Functor class that wraps over your function, and then overload boost::python::detail::get_signature to accept your Functor!

UPDATE: Added support for member functions too!

Example:

#include <boost/shared_ptr.hpp>
#include <boost/python.hpp>
#include <boost/python/signature.hpp>
#include <boost/mpl/vector.hpp>

#include <iostream>
#include <string>
#include <sstream>

static boost::shared_ptr<std::ostringstream> test_stream_data;

std::ostringstream& test_stream()
{
if (!test_stream_data) {
test_stream_data.reset(new std::ostringstream);
}
return *test_stream_data;
}

std::string get_value_and_clear_test_stream()
{
std::string result;
if (test_stream_data) {
result = test_stream_data->str();
}
test_stream_data.reset(new std::ostringstream);
return result;
}

std::string func(int a, double b)
{
std::ostringstream oss;
oss << "func(a=" << a << ", b=" << b << ")";
std::string result = oss.str();
test_stream() << "- In " << result << std::endl;
return result;
}

class MyClass
{
public:
MyClass(std::string p_name)
: m_name(p_name)
{
test_stream() << "- In MyClass::MyClass(p_name=\"" << p_name << "\")" << std::endl;
}

MyClass(MyClass const& p_another)
: m_name(p_another.m_name)
{
test_stream()
<< "- In MyClass::MyClass(p_another=MyClass(\""
<< p_another.m_name << "\"))" << std::endl;
}

~MyClass()
{
test_stream() << "- In MyClass(\"" << this->m_name << "\")::~MyClass()" << std::endl;
}

boost::shared_ptr<MyClass> clone_and_change(std::string p_new_name)
{
test_stream()
<< "- In MyClass(\"" << this->m_name << "\").clone_and_change(p_new_name=\""
<< p_new_name << "\")" << std::endl;

boost::shared_ptr<MyClass> result(new MyClass(*this));
result->m_name = p_new_name;

return result;
}

std::string get_name()
{
test_stream() << "- In MyClass(\"" << this->m_name << "\").get_name()" << std::endl;
return this->m_name;
}

std::string m_name;
};

struct ScopePreAndPostActions
{
ScopePreAndPostActions()
{
test_stream() << "[Before action...]" << std::endl;
}

~ScopePreAndPostActions()
{
test_stream() << "[After action...]" << std::endl;
}
};

template <class FuncType_>
struct FuncWrapper;

// You can code-generate specializations for other arities...

template <class R_, class A0_, class A1_>
struct FuncWrapper<R_ (A0_, A1_)>
{
typedef R_ (*func_type)(A0_, A1_);

typedef typename boost::add_const<typename boost::add_reference<typename A0_>::type>::type AC0_;
typedef typename boost::add_const<typename boost::add_reference<typename A1_>::type>::type AC1_;

func_type m_wrapped_func;

FuncWrapper(func_type p_wrapped_func)
: m_wrapped_func(p_wrapped_func)
{
}

R_ operator()(AC0_ p0, AC1_ p1)
{
ScopePreAndPostActions actions_guard;
return this->m_wrapped_func(p0, p1);
}
};

template <
class R_,
class C_,
class A0_=void,
class A1_=void,
class A2_=void
// ...
>
struct MemberFuncWrapper;

template <class R_, class C_, class A0_>
struct MemberFuncWrapper<R_, C_, A0_>
{
typedef R_ (C_::*member_func_type)(A0_);

typedef typename boost::add_const<typename boost::add_reference<typename A0_>::type>::type AC0_;

member_func_type m_wrapped_method;

MemberFuncWrapper(member_func_type p_wrapped_method)
: m_wrapped_method(p_wrapped_method)
{
}

R_ operator()(C_* p_self, AC0_ p0)
{
ScopePreAndPostActions actions_guard;
return (p_self->*(this->m_wrapped_method))(p0);
return R_();
}
};

namespace boost { namespace python { namespace detail {

// You can code-generate specializations for other arities...

template <class R_, class P0_, class P1_>
inline boost::mpl::vector<R_, P0_, P1_>
get_signature(FuncWrapper<R_ (P0_, P1_)>, void* = 0)
{
return boost::mpl::vector<R_, P0_, P1_>();
}

template <class R_, class C_, class P0_>
inline boost::mpl::vector<R_, C_*, P0_>
get_signature(MemberFuncWrapper<R_, C_, P0_>, void* = 0)
{
return boost::mpl::vector<R_, C_*, P0_>();
}

} } }

// -------------------------------------------------------------------

template <class FuncPtr_>
void make_wrapper(FuncPtr_);

// You can code-generate specializations for other arities...

template <class R_, class A0_, class A1_>
FuncWrapper<R_ (A0_, A1_)> make_wrapper(R_ (*p_wrapped_func)(A0_, A1_))
{
return FuncWrapper<R_ (A0_, A1_)>(p_wrapped_func);
}

template <class R_, class C_, class A0_>
MemberFuncWrapper<R_, C_, A0_> make_wrapper(R_ (C_::*p_wrapped_method)(A0_))
{
return MemberFuncWrapper<R_, C_, A0_>(p_wrapped_method);
}

template <class R_, class C_, class A0_, class A1_>
MemberFuncWrapper<R_, C_, A0_, A1_> make_wrapper(R_ (C_::*p_wrapped_method)(A0_, A1_))
{
return MemberFuncWrapper<R_, C_, A0_, A1_>(p_wrapped_method);
}

using namespace boost::python;

void RegisterTestWrapper()
{
def("GetValueAndClearTestStream", &get_value_and_clear_test_stream);
def("TestFunc", &func);
def(
"TestWrappedFunctor",
make_wrapper(&func)
);

{
class_< MyClass, shared_ptr<MyClass>, boost::noncopyable > c("MyClass", init<std::string>());
c.def("CloneAndChange", &MyClass::clone_and_change);
c.def("GetName", &MyClass::get_name);
c.def("WrappedCloneAndChange", make_wrapper(&MyClass::clone_and_change));
}
}

And on python:

import unittest
from _test_wrapper import GetValueAndClearTestStream, TestFunc, TestWrappedFunctor, MyClass

class Test(unittest.TestCase):

def setUp(self):
GetValueAndClearTestStream()

def testWrapper(self):
self.assertEqual(TestFunc(69, 1.618), 'func(a=69, b=1.618)')
self.assertEqual(GetValueAndClearTestStream(), '- In func(a=69, b=1.618)\n')

self.assertEqual(TestWrappedFunctor(69, 1.618), 'func(a=69, b=1.618)')
self.assertEqual(
GetValueAndClearTestStream(),
(
'[Before action...]\n'
'- In func(a=69, b=1.618)\n'
'[After action...]\n'
),
)

def testWrappedMemberFunction(self):
from textwrap import dedent
x = MyClass("xx")
y = x.WrappedCloneAndChange("yy")
z = y.WrappedCloneAndChange("zz")

self.assertEqual(x.GetName(), "xx")
self.assertEqual(y.GetName(), "yy")
self.assertEqual(z.GetName(), "zz")

self.assertEqual(
GetValueAndClearTestStream(),
dedent('''\
- In MyClass::MyClass(p_name="xx")
[Before action...]
- In MyClass("xx").clone_and_change(p_new_name="yy")
- In MyClass::MyClass(p_another=MyClass("xx"))
[After action...]
[Before action...]
- In MyClass("yy").clone_and_change(p_new_name="zz")
- In MyClass::MyClass(p_another=MyClass("yy"))
[After action...]
- In MyClass("xx").get_name()
- In MyClass("yy").get_name()
- In MyClass("zz").get_name()
'''),
)


Related Topics



Leave a reply



Submit