Boost Python线程分割错误

4
考虑以下简单的 Python 扩展。当启动 start() 时,Foo 将每秒将下一个顺序整数添加到 py::list 中:
#include <boost/python.hpp>
#include <thread>
#include <atomic>

namespace py = boost::python;

struct Foo {
    Foo() : running(false) { } 
    ~Foo() { stop(); }   

    void start() {
        running = true;
        thread = std::thread([this]{
            while(running) {
                std::cout << py::len(messages) << std::end;
                messages.append(py::len(messages));
                std::this_thread::sleep_for(std::chrono::seconds(1));
            }
        });
    }   

    void stop() {
        if (running) {
            running = false;
            thread.join();
        }
    }   

    std::thread thread;
    py::list messages;
    std::atomic<bool> running;
};

BOOST_PYTHON_MODULE(Foo)
{
    PyEval_InitThreads();

    py::class_<Foo, boost::noncopyable>("Foo",
        py::init<>())
        .def("start", &Foo::start)
        .def("stop", &Foo::stop)
    ;   
}

考虑到上述情况,下面这个简单的Python脚本总是会崩溃,甚至无法打印任何内容:
>>> import Foo
>>> f = Foo.Foo()
>>> f.start()
>>> Segmentation fault (core dumped)

核心指向:

namespace boost { namespace python {

    inline ssize_t len(object const& obj)
    {   
        ssize_t result = PyObject_Length(obj.ptr());
        if (PyErr_Occurred()) throw_error_already_set(); // <==
        return result;
    }   

}} // namespace boost::python

地点:

(gdb) inspect obj
$1 = (const boost::python::api::object &) @0x62d368: {<boost::python::api::object_base> = {<boost::python::api::object_operators<boost::python::api::object>> = {<boost::python::def_visitor<boost::python::api::object>> = {<No data fields>}, <No data fields>}, m_ptr = []}, <No data fields>}
(gdb) inspect obj.ptr()
$2 = []
(gdb) inspect result
$3 = 0

为什么在线程中运行时会失败?看起来obj没问题,result被正确设置。为什么会出现PyErr_Occurred()?是谁设置的?

2
这样一个优秀的回答只获得了一个赞,真是太遗憾了...但现在不会再是这样了! - Bhargav Rao
1个回答

13
简而言之,CPython解释器中有一个互斥锁,称为全局解释器锁(GIL)。这个互斥锁防止对Python对象进行并行操作。因此,在任何时刻,最多只允许一个线程(已获取GIL的线程)对Python对象执行操作。当存在多个线程时,调用Python代码而不持有GIL会导致未定义的行为。
在Python文档中,C或C++线程有时被称为外部线程。Python解释器无法控制外部线程。因此,外部线程负责管理GIL以允许与Python线程并发或并行执行。考虑到这一点,让我们来看一下原始代码:
while (running) {
  std::cout << py::len(messages) << std::endl;           // Python
  messages.append(py::len(messages));                    // Python
  std::this_thread::sleep_for(std::chrono::seconds(1));  // No Python
}

如上所述,只有线程拥有GIL时,线程主体中的三行中的两行需要运行。一种常见的处理方法是使用RAII类来帮助管理GIL。例如,使用以下gil_lock类时,当创建一个gil_lock对象时,调用线程将获取GIL。当gil_lock对象被销毁时,它会释放GIL。

/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
  gil_lock()  { state_ = PyGILState_Ensure(); }
  ~gil_lock() { PyGILState_Release(state_);   }
private:
  PyGILState_STATE state_;
};

线程主体可以使用显式作用域来控制锁的生命周期。
while (running) {
  // Acquire GIL while invoking Python code.
  {
    gil_lock lock;
    std::cout << py::len(messages) << std::endl;
    messages.append(py::len(messages));
  }
  // Release GIL, allowing other threads to run Python code while
  // this thread sleeps.
  std::this_thread::sleep_for(std::chrono::seconds(1));
}

这是一个完整的示例,基于原始代码,演示了在显式管理全局解释器锁(GIL)的情况下程序正常工作:

#include <thread>
#include <atomic>
#include <iostream>
#include <boost/python.hpp>

/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
  gil_lock()  { state_ = PyGILState_Ensure(); }
  ~gil_lock() { PyGILState_Release(state_);   }
private:
  PyGILState_STATE state_;
};

struct foo
{
  foo() : running(false) {}
  ~foo() { stop(); }

  void start()
  {
    namespace python = boost::python;
    running = true;
    thread = std::thread([this]
      {
        while (running)
        {
          {
            gil_lock lock; // Acquire GIL.
            std::cout << python::len(messages) << std::endl;
            messages.append(python::len(messages));
          } // Release GIL.
          std::this_thread::sleep_for(std::chrono::seconds(1));
        }
      });
  }

  void stop()
  {
    if (running)
    {
      running = false;
      thread.join();
    }
  }

  std::thread thread;
  boost::python::list messages;
  std::atomic<bool> running;
};

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::class_<foo, boost::noncopyable>("Foo", python::init<>())
    .def("start", &foo::start)
    .def("stop", &foo::stop)
    ;
}

交互使用:

>>> import example
>>> import time
>>> foo = example.Foo()
>>> foo.start()
>>> time.sleep(3)
0
1
2
>>> foo.stop()
>>>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接