3

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

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、Abort等特殊函数),操作系统(在我们的例子中是Windows)是否能够清理所有内容(杀死线程、关闭打开的文件句柄、套接字等)? ETC。)?上述方法可能存在哪些问题?

或更一般的问题:对于全局对象(在 main 之外声明),析构函数是绝对必要的吗?

4

5 回答 5

3

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

这取决于你问的是谁。

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

是的,操作系统会收回一切。它将要求内存、空闲句柄等。

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

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

于 2012-08-17T07:17:35.697 回答
3

一旦进程结束(通过在任务管理器中结束或调用ExitProcess、Abort等特殊函数),操作系统(在我们的例子中是Windows)是否能够清理所有内容(杀死线程、关闭打开的文件句柄、套接字等)? ETC。)?上述方法可能存在哪些问题?

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

但是,如果您的对象正在创建操作系统未清理的资源,那么您就会遇到问题并且需要在您的应用程序中的某处使用析构函数或其他一些明确的清理代码。

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

如果对象的生命周期与应用程序的生命周期紧密耦合,那么根本不进行清理是公认的做法吗?

这是一个主观辩论的问题。我个人的偏好是包含明确的清理代码,并让我创建的每个对象都亲自负责在可行的情况下进行清理。如果应用程序生命周期对象曾经被重构,以至于它们不再存在于对象的生命周期中,我不必回过头来弄清楚是否需要添加以前忽略的清理。我想为了清理我是说我通常更倾向于RAII而不是更务实的YAGNI

于 2012-08-17T07:29:37.357 回答
1

有时它是由规范或仅由“外围设备”的行为强加给您的。也许您的应用程序中有大量缓冲的数据应该真正刷新到磁盘,或者数据库可能会累积“半开”连接,但未明确关闭。

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

1) 如果不编写不需要的额外关闭代码,就很难让应用程序正常工作。

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

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

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

5) 降低性能的内存管理器不是检测泄漏的唯一方法。如果大型对象(例如网络缓冲区)被池化,则很容易判断运行时是否有任何泄漏,而无需依赖在应用程序关闭时发布泄漏报告的 3rd 方内存管理器。像 Valgrind 这样的密集型内存检查器实际上是通过影响整体时序来引起系统问题的。

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

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

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

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

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

于 2012-08-17T08:04:35.113 回答
1

一般来说,现代操作系统在退出时会清理所有进程资源。但在我看来,自己清理干净还是很有礼貌的。(但后来我在 Amiga 上“长大”了,你必须这样做。)

于 2012-08-17T07:19:05.993 回答
0

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

于 2012-08-17T07:31:37.150 回答