清理"application lifetime"对象的必要性

The necessity for cleanup of "application lifetime" objects

本文关键字:必要性 对象 lifetime application 清理      更新时间:2023-10-16

我们继承了大型遗留应用程序,其结构大致如下:

class Application
{
    Foo* m_foo;
    Bar* m_bar;
    Baz* m_baz;
public:
    Foo* getFoo() { return m_foo; }
    Bar* getBar() { return m_bar; }
    Baz* getBaz() { return m_baz; }
    void Init()
    {
        m_foo = new Foo();
        m_bar = new Bar();
        m_baz = new Baz();
        // all of them are singletons, which can call each other
        // whenever they please
        // may have internal threads, open files, acquire
        // network resources, etc.
        SomeManager.Init(this);
        SomeOtherManager.Init(this);
        AnotherManager.Init(this);
        SomeManagerWrapper.Init(this);
        ManagerWrapperHelper.Init(this);
    }
    void Work()
    {
        SomeManagerWrapperHelperWhateverController.Start();
        // it will never finish
    }
    // no destructor, no cleanup
};

创建后的所有管理器将在整个应用程序生存期内保留在那里。应用程序没有关闭或关闭方法,管理器也没有这些方法。因此,复杂的相互依赖关系永远不会得到处理。

问题是:如果对象生存期与应用程序生存期紧密耦合,那么根本不进行清理是否是公认的做法?一旦进程结束(通过在任务管理器中结束它或通过调用特殊功能,如 ExitProcess、中止等),操作系统(在我们的例子中为 Windows)是否能够清理所有内容(杀死线程、关闭打开的文件句柄、套接字等)?上述方法可能存在哪些问题?

或者更通用的问题:析构函数对于全局对象(在 main 外部声明)是否绝对必要?

操作系统(在我们的例子中是Windows)是否能够清理 所有内容(终止线程、关闭打开的文件句柄、套接字等)一次 该过程结束(通过在任务管理器中结束它或通过调用 Special 退出进程、中止等函数)?可能出现的问题是什么 用上述方法?

只要您的对象没有初始化操作系统未清理的任何资源,那么无论您是否显式清理都没有任何实际区别,因为当您的进程终止时,操作系统会为您清理。

但是,如果你的对象正在创建未作系统清理的资源,那么你就遇到了问题,需要在应用中的某个地方使用析构函数或其他一些显式清理代码。

考虑其中一个对象是否在某些远程服务(例如数据库)上创建会话。当然,操作系统不会神奇地知道这已经完成,或者当你的进程死去时如何清理它们,所以这些会话将保持打开状态,直到有什么东西杀死它们(DBMS 本身可能通过强制执行一些超时阈值或其他)。如果你的应用是资源的小型用户,并且你在大型基础结构上运行,也许不是问题 - 但是如果你的应用创建然后孤立了足够的会话,那么该远程服务上的资源争用可能会开始成为一个问题。

如果对象生存期与应用程序紧密耦合 一辈子,根本不清理是公认的做法吗?

这是一个主观辩论的问题。我个人的偏好是包含显式清理代码,并让我创建的每个对象亲自负责在可行的情况下自行清理。如果对应用程序生存期对象进行了重构,使它们在对象的生存期内不再存在,则我不必返回并确定是否需要添加以前省略的清理。我想为了清理,我是说我通常更喜欢 RAII 而不是更务实的 YAGNI。

根本不进行清理是公认的做法吗

这取决于你问谁。

操作系统(在我们的例子中是Windows)是否能够清理 所有内容(终止线程、关闭打开的文件句柄、套接字等)一次 进程结束

是的,操作系统将收回所有内容。它将占用内存,空闲句柄等。

上述方法可能存在哪些问题

可能的问题之一是,如果您使用内存泄漏检测器,它会不断显示您有泄漏。

通常,现代操作系统会在退出时清理所有进程资源。但在我看来,清理自己还是很有礼貌的。(但后来我在阿米加"长大",你必须在那里做。

有时它是由规范或仅由"外围设备"的行为强加给您的。 也许您的应用程序中缓冲了大量数据,这些数据实际上应该刷新到磁盘,或者数据库可能会累积"半打开"连接未显式关闭。

除此之外,正如@cnicutar所说,这取决于你问谁。 我坚定地站在"不要打扰"阵营,原因如下:

1)无论如何,如果不编写不需要的额外关机代码,就很难让应用程序运行。

2)你写的代码越多,错误就越多,你需要做的测试就越多。 您可能需要在多个操作系统版本中测试此类代码:(

3)操作系统开发人员花了很长时间来确保应用程序始终可以在需要时关闭(例如,通过任务管理器),而不会对系统的其余部分产生任何整体影响。 如果操作系统中已经存在某些功能,为什么不利用它呢?

4)线程带来了一个特定的问题 - 它们可以处于任何状态。 它们可能运行在与启动应用关闭的线程不同的内核上,或者可能在系统调用中被阻止。 虽然操作系统很容易确保在释放任何内存、关闭句柄等之前终止所有线程,但很难以安全可靠的方式从用户代码中停止此类线程。

5)性能消耗内存管理器并不是检测泄漏的唯一方法。如果大型对象(例如网络缓冲区)已池化,则很容易判断运行时是否有任何泄漏,而无需依赖在应用程序关闭时发出泄漏报告的第三方内存管理器。像 Valgrind 这样的密集内存检查器实际上会影响整体时序而导致系统问题。

6)根据经验,我为Windows编写的每个没有显式关闭代码的应用程序在用户单击"红叉"边框图标时都会立即完全关闭。 这包括在具有数千个连接客户端的多核机箱上运行的繁忙、复杂的 IOCP 服务器。

7) 假设已经完成了合理的测试阶段 - 包括加载/浸泡测试 - 不难区分泄漏的应用程序和选择不释放它在关闭时使用的内存的应用程序。 漏勺应用程序将显示内存/句柄/随运行时总是增加的任何内容。

8)不明显的小的,偶尔的泄漏不值得花费大量时间。 无论如何,大多数Windows盒子每个月都会重新启动(补丁星期二)。

9)不透明的库通常是由像我这样的开发人员编写的,因此无论如何都会在关闭时生成虚假的"泄漏报告"。

设计/编写/调试/测试关闭代码只是为了清理内存报告是一种昂贵的奢侈品,我可以没有:)

您应该为每个对象单独确定。如果对象需要在清理时执行特殊操作(例如将缓冲区刷新到磁盘),则除非您明确处理它,否则不会发生这种情况。