【问题标题】:RAII can not really guarantee to prevent resource leak, can it?RAII 并不能真正保证防止资源泄漏,可以吗?
【发布时间】:2014-05-08 15:32:03
【问题描述】:

如果这个问题太愚蠢,请原谅我。使用 RAII 的最常见示例是:

void func(){
  // create some object pointer using any smart pointer
  // do some operation that may throw

  return;
}
// whether method returns from the *return* statement or because of any exception it is guaranteed that the memory will be released

This article 说(如果我理解正确的话),如果运行时系统知道没有异常处理程序可以在抛出异常后捕获异常它可能跳过调用自动对象的析构函数。

还有一个针对该问题的建议解决方案,即在main 中使用catch(..)

现在我担心的是,如果不使用建议的 解决方案,那么即使在使用 RAII 之后也可能存在资源泄漏。并且存在无法应用解决方案的情况(例如创建将被其他人使用的库)。在这种情况下,可能会出现严重问题,例如损坏包含有价值信息的文件。

我们真的应该关注这个问题吗?或者我只是错过了什么?

【问题讨论】:

  • 如果你的程序崩溃了,你真的认为泄漏是你最大的问题吗?
  • 这篇文章我没看过,但是如果没有异常处理程序,是不是程序会被终止,所以资源还是会被释放? (假设它不是像临时文件或数据库锁这样的进程外的东西。)
  • 如果我拔掉你的机器,析构函数不会被调用。我是认真的:如果你的析构函数对远程 API 进行查询,你不能假设你会得到完美平衡的锁定/解锁调用!
  • 标准的相关部分是15.3/9:如果没有找到匹配的处理程序,则调用函数std::terminate();在此调用 std::terminate() 之前是否展开堆栈是实现定义的。
  • 我发现任何涉及需要非平凡清理的资源的操作(删除临时、注销服务器等,任何不能简单地由OS)通常必须在编写时具有强大的异常安全保证,而单独的 RAII 很少是足够的(除了一些聪明的例子)。强大的异常安全通常涉及包含一些清理和回滚代码的包罗万象(或所有可能的异常)。当您只控制资源访问(RAII类)而不是整体操作时,不可能提供强有力的保证。

标签: c++ exception raii


【解决方案1】:

为了使您的关注有效,您需要某种资源可以被 RAII 清理,但操作系统不会在什么时候清理std::terminate 被调用并且你的进程终止了。

那么,让我们来看看你可以合理地使用 RAII 来清理哪些资源:

  1. 进程本地内存:操作系统会清理它
  2. 打开文件:操作系统将关闭它们,并刷新所有已写入的内容,但您留给 RAII dtor 的任何提交/完成样式操作都不会发生
  3. 打开的套接字:操作系统将关闭它们,但如果您的 RAII dtor 应该发送友好的注销/再见,则不会发生
  4. 共享内存:同样,操作系统级资源将被释放,但不会执行任何显式清理或一致性代码
  5. 等。 (特别是,Alf 提出了一些我没想到的更多外部可见的资源)

所以问题通常不在于 resources,它通常由操作系统发布,而是 semantics,您的 RAII dtor 应该保证某种干净的状态共享资源(共享内存、文件或网络流)。

我们真的应该关心这个问题吗?

好吧,无论如何我们都应该关注正确的程序语义。如果您的程序有一些您需要保证的外部副作用,那么保证 RAII 清理的包罗万象(至少围绕相关代码)是您最关心的问题。

【讨论】:

  • 看来这个答案集中在操作系统将清理的资源上。其他资源包括临时文件(不是它们的打开状态,而是它们的存在)、注入的 DLL、设备状态。需要执行析构函数的其他原因包括日志记录和断点。
  • 我不知道为什么如果进程崩溃,日志记录和断点是必不可少的,因为我宁愿在没有堆栈展开的情况下进行核心转储。但是,它很可能是我没有使用过的习惯用法,或者是特定于平台的(例如临时文件和共享库处理)。
  • 您提到的大部分内容都是特定于平台的观点(事后调试,自动删除临时文件)。但你说得对,DLL 注入是特定于平台的。 c++ 旨在迎合所有平台,就像 c 一样,尽管 c++11 在这方面失败了很多(例如核心语言 wchar_t 与 windows 不兼容),但恕我直言,我们应该假装 c++ 真的不只是 *nix,并采取行动基础。
  • 可能是这样,谢谢。无论如何,我希望将流程资源问题与外部副作用问题分开。也许编辑更清晰一些。
【解决方案2】:

我在这里假设您不太关心诸如内存泄漏之类的事情,而是更关心数据损坏。

您需要仔细分析您的设计和应用程序,但我认为理论上您可能需要一种“最后机会”类型的故障保护,以便在出现严重程序错误的情况下启动导致未捕获的异常。在这种情况下,您可以替换 std::terminate

也就是说,如果您真的担心文件损坏,那么您所描述的将是不充分的。防止文件损坏的方法是在考虑完成操作(即,提交一个操作)。

【讨论】:

  • 如果问题只是你的程序终止,你不需要fsync;只需刷新输出流就足够了。 (此外,没有标准方法可以从std::filebuf 获取fsync 所需的系统级文件描述符。)
【解决方案3】:

如果你没有捕捉到异常,程序将会终止。 在这种情况下,您担心的大部分资源 (内存,互斥锁等)将被操作系统清理,所以你 不必担心。最大的例外是临时文件。 输出文件也可能是一个问题,因为它们可能是 不完整或不一致,你不想离开 不完整或不一致的文件可供某人使用 不小心使用。 (我通常使用OutputFile 类 包装std::ofstream,并在析构函数中删除文件 如果 commit 没有被调用,或者如果 closecommit 失败。)

当然,如果有你没想到的异常,那就是 程序出现严重错误;我经常发现它对 调试或尝试时不要删除临时文件 找出代码不起作用的原因。 (此类例外将 当然,永远不会在用户站点发生,因为您将拥有 在发布之前对程序进行了充分的测试:-)。)

如果真的有问题,可以使用std::set_terminate 设置一个终止处理程序,它可以进行任何最后一分钟的清理。 (但请注意,在这种情况下,不确定是否 堆栈是否已展开,因此您可能会遇到问题 确定需要清理的内容。)

【讨论】:

    【解决方案4】:

    回复

    “我们真的应该关注这个问题吗?”

    这取决于上下文。

    如果您使用析构函数断点或登录调试,则需要调用相关的析构函数。同样,如果析构函数正在为崩溃后重新实例化的“凤凰鸟”进程保存关键状态。如果可能的话,最好删除不需要恢复的临时文件,而不是在崩溃后放置。

    另一方面,由于解决方案非常简单 - 一个 try-catch 围绕一些调用代码,例如在main 上——这不是一个真正的实际问题。这更像是一件需要注意的事情。例如,不要期望析构函数一定会在因未处理异常而崩溃的其他代码中执行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-10-23
      • 1970-01-01
      • 2018-09-13
      • 2014-04-01
      • 2014-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多