PyBind11析构函数未被调用?

4
我有一个用PyBind11封装的c++类。问题是:当Python脚本结束时,c++析构函数不会自动调用。这会导致退出时不整洁,因为析构函数需要释放网络资源。
为了解决这个问题,需要显式地删除Python对象,但我不知道为什么要这样做!
请问有人能解释这里出了什么问题,并且如何在垃圾回收时自动调用destructorPybind11绑定代码:
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

请查看此帖子:https://stackoverflow.com/questions/38228170/c-destructor-calling-of-boostpython-wrapped-objects - P.W
3个回答

1

所以几年后,我通过启用Python上下文管理器with支持并添加__enter____exit__方法处理到我的PyBind11代码中来解决了这个问题:

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 功能非常重要。

  2. 传递给 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++对象的析构函数,从而平滑释放网络资源。

0

GoFaster的解决方案很有帮助,也是正确的方法,但我只想澄清和纠正他们的说法:

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

所以资源已经构建,获取内存,并在上下文管理器中访问。到目前为止都很好。然而,在上下文管理器之外仍然可以访问内存(并且在析构函数被调用之前,这由Python运行时决定,除非我们使用del强制控制,否则完全违背了上下文管理器的初衷。

但是我们实际上没有在__exit__中释放资源。如果您取消注释该函数中的pi.reset(),您将得到以下结果:

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 资源的包装器。


感谢您的深入评论和最佳实践建议。我在C++端的exit()函数中添加了delete this;以自毁该实例。我注意到这当然会直接调用C++析构逻辑,在我的情况下释放资源,但我也注意到对象仍然存在,并且稍后会有第二次析构调用,这可能来自Python端。我怀疑return py::cast(this);返回一个共享指针以促进这种长寿行为。 - GoFaster
delete this; 仅适用于堆分配的内存,因此可能会调用 UB,您可能意味着 this->~listener();,但您已经注意到这会导致两个析构函数调用。在我的示例中,只有 ManagedResource 类中的成员变量在 exit 中被释放,而不是类本身。您可能需要将 listener 类放入包装类中。 - virgesmith
我真傻,是的,在exit()函数中添加this->~Listener();会调用析构函数,但也会导致稍后第二次析构函数的调用。 - GoFaster

0

正如评论中提到的那样,这种行为的直接原因是Python垃圾回收器:当一个对象的引用计数器降至零时,垃圾回收器可能销毁该对象(从而调用c++析构函数),但它不一定要在那个时刻执行。

这个想法在这里的答案中得到了更充分的阐述:

https://stackoverflow.com/a/38238013/790979

正如上面的链接中提到的那样,如果你需要在 Python 中清理对象生命周期结束时的资源,一个好的解决方案是上下文管理器,你可以在对象的包装器(无论是在 pybind11 还是在 Python 本身中)中定义 __enter____exit__,让 __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)

感谢@charleslparker 的答案和解释。虽然我还没有解决这个问题,因为我很难找到有关如何在PyBind11绑定代码中实现__enter____exit__的示例和文档。 - GoFaster

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