Как вызвать Python из потока boost?

У меня есть приложение Python, которое вызывает Cython-библиотеку python boost, и все это работает. Тем не менее, у меня есть сценарий обратного вызова C ++ для Python, где C ++ из потока boost вызывает python, и я получаю нарушение доступа на стороне C ++. Если я делаю точно такой же обратный вызов, используя поток python, он отлично работает. Поэтому я подозреваю, что не могу просто вызвать Python из C ++, используя поток boost, но вам нужно сделать что-то дополнительное для его работы?

Наиболее вероятным виновником является то, что Global Interpreter Lock (GIL) не удерживается нитью при вызове кода Python, что приводит к неопределенному поведению. Проверьте все пути, которые делают прямые или косвенные вызовы Python, приобретают GIL перед вызовом кода Python.


GIL – это мьютекс вокруг интерпретатора CPython. Этот мьютекс предотвращает параллельные операции над объектами Python. Таким образом, в любой момент времени максимум одного потока, тот, который приобрел GIL, разрешен для выполнения операций над объектами Python. Когда присутствуют несколько потоков, вызывать код Python, не удерживая GIL, приводит к неопределенному поведению.

C или C ++ потоки иногда упоминаются как чужие потоки в документации Python. У интерпретатора Python нет возможности управлять чужой нитью. Поэтому чужие потоки отвечают за управление GIL, чтобы разрешать параллельное или параллельное выполнение с потоками Python. Нужно скрупулезно рассмотреть:

  • Распаковка стека, поскольку Boost.Python может генерировать исключение.
  • Косвенные вызовы на Python, такие как конструкторы-копии или деструкторы

Одним из решений является обертка обратных вызовов Python с настраиваемым типом, который знает об управлении GIL.


Использование класса RAII для управления GIL обеспечивает элегантное решение, исключающее исключение. Например, при следующем классе with_gil , когда with_gil объект with_gil , вызывающий поток получает GIL. Когда объект with_gil разрушен, он восстанавливает состояние GIL.

 /// @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_; }; 

И его использование:

 { with_gil gil; // Acquire GIL. // perform Python calls, may throw } // Restore GIL. 

Имея возможность управлять GIL через with_gil , следующим шагом является создание функтора, который надлежащим образом управляет GIL. Следующий класс py_callable будет обертывать boost::python::object и приобретать GIL для всех путей, в которых вызывается код Python:

 /// @brief Helper type that will manage the GIL for a python callback. /// /// @detail GIL management: /// * Acquire the GIL when copying the `boost::python` object /// * The newly constructed `python::object` will be managed /// by a `shared_ptr`. Thus, it may be copied without owning /// the GIL. However, a custom deleter will acquire the /// GIL during deletion /// * When `py_callable` is invoked (operator()), it will acquire /// the GIL then delegate to the managed `python::object` class py_callable { public: /// @brief Constructor that assumes the caller has the GIL locked. py_callable(const boost::python::object& object) { with_gil gil; object_.reset( // GIL locked, so it is safe to copy. new boost::python::object{object}, // Use a custom deleter to hold GIL when the object is deleted. [](boost::python::object* object) { with_gil gil; delete object; }); } // Use default copy-constructor and assignment-operator. py_callable(const py_callable&) = default; py_callable& operator=(const py_callable&) = default; template <typename ...Args> void operator()(Args... args) { // Lock the GIL as the python object is going to be invoked. with_gil gil; (*object_)(std::forward<Args>(args)...); } private: std::shared_ptr<boost::python::object> object_; }; 

Управляя boost::python::object на свободном пространстве, можно свободно копировать shared_ptr без необходимости удерживать GIL. Это позволяет нам безопасно использовать созданный по умолчанию экземпляр-конструктор, оператор присваивания, деструктор и т. Д.

Можно использовать py_callable следующим образом:

 // thread 1 boost::python::object object = ...; // GIL must be held. py_callable callback(object); // GIL no longer required. work_queue.post(callback); // thread 2 auto callback = work_queue.pop(); // GIL not required. // Invoke the callback. If callback is `py_callable`, then it will // acquire the GIL, invoke the wrapped `object`, then release the GIL. callback(...); 

Вот полный пример, демонстрирующий, что расширение Python вызывает объект Python в качестве обратного вызова из потока C ++:

 #include <memory> // std::shared_ptr #include <thread> // std::this_thread, std::thread #include <utility> // std::forward #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 Helper type that will manage the GIL for a python callback. /// /// @detail GIL management: /// * Acquire the GIL when copying the `boost::python` object /// * The newly constructed `python::object` will be managed /// by a `shared_ptr`. Thus, it may be copied without owning /// the GIL. However, a custom deleter will acquire the /// GIL during deletion /// * When `py_callable` is invoked (operator()), it will acquire /// the GIL then delegate to the managed `python::object` class py_callable { public: /// @brief Constructor that assumes the caller has the GIL locked. py_callable(const boost::python::object& object) { with_gil gil; object_.reset( // GIL locked, so it is safe to copy. new boost::python::object{object}, // Use a custom deleter to hold GIL when the object is deleted. [](boost::python::object* object) { with_gil gil; delete object; }); } // Use default copy-constructor and assignment-operator. py_callable(const py_callable&) = default; py_callable& operator=(const py_callable&) = default; template <typename ...Args> void operator()(Args... args) { // Lock the GIL as the python object is going to be invoked. with_gil gil; (*object_)(std::forward<Args>(args)...); } private: std::shared_ptr<boost::python::object> object_; }; 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("call_later", +[](int delay, python::object object) { // Create a thread that will invoke the callback. std::thread thread(+[](int delay, py_callable callback) { std::this_thread::sleep_for(std::chrono::seconds(delay)); callback("spam"); }, delay, py_callable{object}); // Detach from the thread, allowing caller to return. thread.detach(); }); } 

Интерактивное использование:

 >>> import time >>> import example >>> def shout(message): ... print message.upper() ... >>> example.call_later(1, shout) >>> print "sleeping"; time.sleep(3); print "done sleeping" sleeping SPAM done sleeping