c++ 未调用PyBind11析构函数?

z3yyvxxp  于 2022-12-05  发布在  其他
关注(0)|答案(3)|浏览(240)

我有一个用PyBind11 Package 的c++类。问题是:当Python脚本结束时,c++destructor没有被自动调用。这会导致一个不整洁的退出,因为网络资源需要被析构函数释放。
作为一种解决办法,有必要显式删除Python对象,但我不明白为什么!
请有人解释一下这里出了什么问题,以及如何在Python对象被垃圾收集时自动调用destructor
Pybind 11绑定代码:

py::class_<pcs::Listener>(m, "listener")
    .def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
    Monitors network traffic.

    When a desired data source is detected a client instance is connected to consume the data stream.

    Reconstructs data on receipt, like a jigsaw.  Makes requests to fill any gaps.  Verifies the data as sequential.

    Data is output by callback to Python.  Using the method specified in the constructor, which must accept a string argument.
)pbdoc");

在Python中:

#Function to callback
def print_string(str):
    print("Python; " + str)

lstnr = listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose)

#Run for a minute
cnt = 0
while cnt < 60:
    cnt += 1
    time.sleep(1)

#Need to call the destructor explicity for some reason    
del lstnr
qacovj5a

qacovj5a1#

正如在注解中提到的,这种行为的直接原因是Python垃圾收集器:当一个对象的引用计数器为零时,垃圾收集器 * 可能 * 销毁该对象(从而调用c++析构函数),但它 * 不必在那个时候 * 这么做。
这个想法在下面的回答中得到了更充分的阐述:
https://stackoverflow.com/a/38238013/790979
正如在上面的链接中提到的,如果你在Python中的对象生命周期结束时需要清理,一个不错的解决方案是context management,你可以在对象的 Package 器中定义__enter____exit__(在pybind11或Python本身中),让__exit__释放网络资源,然后,在Python客户端代码中,类似于:

with listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose) as lstnr:
    # Run for a minute
    cnt = 0
    while cnt < 60:
        cnt += 1
        time.sleep(1)
nzk0hqpo

nzk0hqpo2#

所以几年后,我通过在PyBind11代码中添加__enter____exit__方法处理来启用Python上下文管理器with支持,从而修复了这个问题:

py::class_<pcs::Listener>(m, "listener")
.def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
    Monitors network traffic.

    When a desired data source is detected a client instance is connected to consume the data stream.
    
    Specify 'type' as 'string' or 'market_data' to facilitate appropriate handling of BarData or string messages.

    Reconstructs data on receipt, like a jigsaw.  Makes requests to fill any gaps.  Verifies the data as sequential.

    Data is output by callback to Python.  Using the method specified in the constructor, which must accept a string argument.
)pbdoc")
.def("__enter__", &pcs::Listener::enter, R"pbdoc(
    Python 'with' context manager support.
)pbdoc")    
.def("__exit__", &pcs::Listener::exit, R"pbdoc(
    Python 'with' context manager support.
)pbdoc");

在C++类中添加了相应的函数,如下所示:

//For Python 'with' context manager
auto enter(){std::cout << "Context Manager: Enter" << std::endl; return py::cast(this); }//returns a pointer to this object for 'with'....'as' python functionality
auto exit(py::handle type, py::handle value, py::handle traceback){ std::cout << "Context Manager: Exit: " << type << " " << value << " " << traceback <<  std::endl; }


1.从enter()返回的指针值对于with .... as语句中的as功能非常重要。
1.传递给exit(py::handle type, py::handle value, py::handle traceback)的参数是有用的调试信息。
Python用法:

with listener(cb, endpoint, clientCertPath, serverCertPath, proxyCertPath, desiredSources, type, enableCurve, enableVerbose):
cnt = 0
while cnt < 10:
    cnt += 1
    time.sleep(1)

Python上下文管理器现在调用C++对象上的析构函数,从而顺利地释放网络资源。

2exbekwf

2exbekwf3#

上面GoFaster的解决方案是有帮助和正确的方法,但我只是想澄清和纠正他们的Assert,
Python上下文管理器现在调用C对象上的析构函数,从而顺利释放网络资源
这是不正确的。上下文管理器只保证__exit__将被调用,而不保证任何析构函数将被调用。让我来演示一下-这是一个用C
实现的托管资源:

class ManagedResource
{
public:
    ManagedResource(int i) : pi(std::make_unique<int>(i))
    {
        py::print("ManagedResource ctor");
    }

    ~ManagedResource()
    {
        py::print("ManagedResource dtor");
    }

    int get() const { return *pi; }

    py::object enter()
    {
        py::print("entered context manager");
        return py::cast(this);
    }

    void exit(py::handle type, py::handle value, py::handle traceback)
    {
        // release resources
        // pi.reset();
        py::print("exited context manager");
    }

private:
    std::unique_ptr<int> pi;
};

Python绑定:

py::class_<ManagedResource>(m, "ManagedResource")
    .def(py::init<int>())
    .def("get", &ManagedResource::get)
    .def("__enter__", &ManagedResource::enter, R"""(
        Enter context manager.
    )""")
    .def("__exit__", &ManagedResource::exit, R"""(
        Leave context manager.
    )""");

和一些python测试代码(注意上面的代码 * 还没有 * 释放__exit__中的资源):

def f():
    with ManagedResource(42) as resource1:
        print(f"get = {resource1.get()}")
    print(f"hey look I'm still here {resource1.get()}") # not destroyed

if __name__ == "__main__":
    f()
    print("end")

其产生:

ManagedResource ctor
entered context manager
get = 42
exited context manager
hey look I'm still here 42
ManagedResource dtor
end

因此,资源被构造,获取内存,并在上下文管理器中被访问。到目前为止,一切都很好。然而,内存仍然可以在上下文管理器之外被访问(在调用destuctor之前,这是由python运行时决定的,不在我们的控制范围内,除非我们用del强制它,这完全破坏了上下文管理器的作用。
但是我们实际上并没有释放__exit__中的资源。

ManagedResource ctor
entered context manager
get = 42
exited context manager
Segmentation fault (core dumped)

这一次,当您在上下文管理器外部调用get()时,ManagedResource对象本身仍然没有被析构,但是它内部的资源已经被释放,
而且还有更大的危险:如果在with块外创建ManagedResource,则会泄漏资源,因为__exit__永远不会被调用。要解决此问题,需要将从构造函数获取资源的操作推迟到__enter__方法,并检查get中是否存在该资源。
简而言之,这个故事的寓意是:

  • 即使对于上下文管理器,也不能依赖于python对象何时/何地被析构
  • 您 * 可以 * 在上下文管理器 * 内 * 控制资源的获取和释放
  • 应该在__enter__方法中获取资源,而不是在构造函数中
  • 应该在__exit__方法中释放资源,而不是析构函数
  • 您应该在访问资源的周围设置足够的防护措施

上下文管理的对象本身不是RAII资源,而是RAII资源的 Package 器。

相关问题