6

I'm writing a python program that includes a c++ module (.so, using boost.python).
I'm starting several python threads that run a c++ function.

This is how the C++ code looks like:

#include <boost/python.hpp>
using namespace boost;
void f(){
    // long calculation

    // call python function

    // long calculation
}

BOOST_PYTHON_MODULE(test)
{
    python::def("f", &f);
}

And the python code:

from test import f
t1 = threading.Thread(target=f)
t1.setDaemon(True)
t1.start()
print "Still running!"

I encounter a problem: the "Still running!" message isn't shown, and I found out that the c++ thread is holding the GIL.

What is the best method of handling the GIL in my case of running c++ code from python code?

Thanks! Gal

boardrider
  • 5,882
  • 7
  • 49
  • 86
Gal Malka
  • 307
  • 3
  • 12
  • I don't know boost::python (but thanks for mentioning the name, it looks very interesting), but this answer looks like it might solve your question: [`struct no_gil`](http://stackoverflow.com/a/18648366/416224). – Kijewski Dec 14 '16 at 03:25
  • Thanks. I saw those saveThread/ restoreThread methods, but I still need to reaquire the Gil inside my function to call some Python code. – Gal Malka Dec 14 '16 at 07:35

1 Answers1

7

I often find that using RAII-style classes to manage the Global Interpreter Lock (GIL) provides an elegant exception-safe solution.

For example, with the following with_gil class, when a with_gil object is created, the calling thread acquires the GIL. When the with_gil object is destructed, it restores the GIL state.

/// @brief Guard that will acquire the GIL upon construction, and
///        restore its state upon destruction.
class with_gil
{
public:
  with_gil()  { state_ = PyGILState_Ensure(); }
  ~with_gil() { PyGILState_Release(state_);   }

  with_gil(const with_gil&)            = delete;
  with_gil& operator=(const with_gil&) = delete;
private:
  PyGILState_STATE state_;
};

And the complementary without_gil class does the opposite:

/// @brief Guard that will unlock the GIL upon construction, and
///        restore its staet upon destruction.
class without_gil
{
public:
  without_gil()  { state_ = PyEval_SaveThread(); }
  ~without_gil() { PyEval_RestoreThread(state_); }

  without_gil(const without_gil&)            = delete;
  without_gil& operator=(const without_gil&) = delete;
private:
  PyThreadState* state_;
};

Their usage within a function could be as follows:

void f()
{
  without_gil no_gil;       // release gil
  // long calculation
  ...

  {
    with_gil gil;           // acquire gil
    // call python function
    ...
  }                         // restore gil (release)

  // long calculation
  ...
}                           // restore gil (acquire)

One can also use a higher level convenient class to provide a std::lock_guard like experience. The GIL acquisition and release, save and restore semantics are slightly different than a normal mutex. Hence, the gil_guard interface is different:

  • gil_guard.acquire() will acquire the GIL
  • gil_guard.release() will release the GIL
  • gil_guard_restore() will restore the previous state
/// @brief Guard that provides higher-level GIL controls.
class gil_guard
{
public:
  struct no_acquire_t {} // tag type used for gil acquire strategy
  static no_acquire;

  gil_guard()             { acquire(); }
  gil_guard(no_acquire_t) { release(); }
  ~gil_guard()            { while (!stack_.empty()) { restore(); } }

  void acquire()          { stack_.emplace(new with_gil); }
  void release()          { stack_.emplace(new without_gil); }
  void restore()          { stack_.pop(); }

  static bool owns_gil()
  {
    // For Python 3.4+, one can use `PyGILState_Check()`.
    return _PyThreadState_Current == PyGILState_GetThisThreadState();
  }

  gil_guard(const gil_guard&)            = delete;
  gil_guard& operator=(const gil_guard&) = delete;

private:
  // Use std::shared_ptr<void> for type erasure.
  std::stack<std::shared_ptr<void>> stack_;
};

And its usage would be:

void f()
{
  gil_guard gil(gil_guard::no_acquire); // release gil
  // long calculation
  ...

  gil.acquire();                        // acquire gil
  // call python function
  ...
  gil.restore();                        // restore gil (release)

  // long calculation
  ...
}                                       // restore gil (acquire)

Here is a complete example demonstrating GIL management with these auxiliary classes:

#include <cassert>
#include <iostream> // std::cout, std::endl
#include <memory>   // std::shared_ptr
#include <thread>   // std::this_thread
#include <stack>    // std::stack
#include <boost/python.hpp>

/// @brief Guard that will acquire the GIL upon construction, and
///        restore its state upon destruction.
class with_gil
{
public:
  with_gil()  { state_ = PyGILState_Ensure(); }
  ~with_gil() { PyGILState_Release(state_);   }

  with_gil(const with_gil&)            = delete;
  with_gil& operator=(const with_gil&) = delete;
private:
  PyGILState_STATE state_;
};

/// @brief Guard that will unlock the GIL upon construction, and
///        restore its staet upon destruction.
class without_gil
{
public:
  without_gil()  { state_ = PyEval_SaveThread(); }
  ~without_gil() { PyEval_RestoreThread(state_); }

  without_gil(const without_gil&)            = delete;
  without_gil& operator=(const without_gil&) = delete;
private:
  PyThreadState* state_;
};

/// @brief Guard that provides higher-level GIL controls.
class gil_guard
{
public:
  struct no_acquire_t {} // tag type used for gil acquire strategy
  static no_acquire;

  gil_guard()             { acquire(); }
  gil_guard(no_acquire_t) { release(); }
  ~gil_guard()            { while (!stack_.empty()) { restore(); } }

  void acquire()          { stack_.emplace(new with_gil); }
  void release()          { stack_.emplace(new without_gil); }
  void restore()          { stack_.pop(); }

  static bool owns_gil()
  {
    // For Python 3.4+, one can use `PyGILState_Check()`.
    return _PyThreadState_Current == PyGILState_GetThisThreadState();
  }

  gil_guard(const gil_guard&)            = delete;
  gil_guard& operator=(const gil_guard&) = delete;

private:
  // Use std::shared_ptr<void> for type erasure.
  std::stack<std::shared_ptr<void>> stack_;
};

void f()
{
  std::cout << "in f()" << std::endl;

  // long calculation
  gil_guard gil(gil_guard::no_acquire);
  assert(!gil.owns_gil());
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  std::cout << "calculating without gil..." << std::endl;

  // call python function
  gil.acquire();
  assert(gil.owns_gil());
  namespace python = boost::python;
  python::object print =
  python::import("__main__").attr("__builtins__").attr("print");
    print(python::str("calling a python function"));
  gil.restore();

  // long calculation
  assert(!gil.owns_gil());
  std::cout << "calculating without gil..." << std::endl;
}

BOOST_PYTHON_MODULE(example)
{
  // Force the GIL to be created and initialized.  The current caller will
  // own the GIL.
  PyEval_InitThreads();

  namespace python = boost::python;
  python::def("f", +[] {
    // For exposition, assert caller owns GIL before and after
    // invoking function `f()`.
    assert(gil_guard::owns_gil());
    f();
    assert(gil_guard::owns_gil());
  });
}

Interactive usage:

>>> import threading
>>> import example
>>> t1 = threading.Thread(target=example.f)
>>> t1.start(); print "Still running"
in f()
Still running
calculating without gil...
calling a python function
calculating without gil...
>>> t1.join()
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Are you sure about `std::shared_ptr`? If I'm not mistaken, then the correct d'tor won't be called. Wouldn't `boost::variant` be a more easy to understand solution? – Kijewski Dec 16 '16 at 12:07
  • 1
    @Kay `std::shared_ptr` will call the appropriate destructor. The constructor is also a template, taking the form of`std::shared_ptr::shared_ptr(Y* p)`. The standard requires that `p` is convertible to `T*`, and that the expression `delete p` be well formed. I agree that `boost::variant` may be easier to understand at glance. However, performing type erasure dissuades anyone from manipulating the elements. – Tanner Sansbury Dec 16 '16 at 13:36
  • Detailed code + explanations. Thanks. Should be listed in the FAQ as an example of a good SO answer. This comment, however, is not. – Erik Aronesty Dec 20 '18 at 13:54