【问题标题】:How to solve a problem in using RAII code and non-RAII code together in C++?如何解决在 C++ 中同时使用 RAII 代码和非 RAII 代码的问题?
【发布时间】:2009-12-30 07:53:13
【问题描述】:

我们有 3 个不同的库,每个库都由不同的开发人员开发,而且每个库(大概)都经过精心设计。但由于有些库使用 RAII,有些不使用,有些库是动态加载的,而另一些则不是 - 它不起作用。

每个开发人员都说他所做的是正确的,并且仅针对这种情况进行方法更改(例如在 B 中创建 RAII 单例)可以解决问题,但看起来只是一个丑陋的补丁。

您建议如何解决这个问题?

请看代码理解问题:


我的代码:

static A* Singleton::GetA()
{
    static A* pA = NULL;
    if (pA == NULL)
    {
        pA = CreateA();
    }
    return pA;
}

Singleton::~Singleton()  // <-- static object's destructor, 
                         // executed at the unloading of My Dll.
{
     if (pA != NULL)
     {
         DestroyA();
         pA = NULL;
     }
}

“A”代码(在另一个 Dll 中,静态链接到我的 Dll):

A* CreateA()
{
    // Load B Dll library dynamically
    // do all other initializations and return A*
}
void DestroyA()
{
    DestroyB();
}

“B”代码(在另一个 Dll 中,从 A 动态加载):

static SomeIfc* pSomeIfc;
void DestroyB()
{
    if (pSomeIfc != NULL)
    {
        delete pSomeIfc;  // <-- crashes because the Dll B was unloaded already,
                          // since it was loaded dynamically, so it is unloaded
                          // before the static Dlls are unloaded.
        pSomeIfc = NULL;
    }
}

【问题讨论】:

  • 我不清楚是什么导致了“B”DLL 的过早卸载,你能澄清一下吗?临时解决方案似乎正在修改 B DLL 的卸载点,以更恰当地匹配 A 库的使用范围。例如,如果 DLL 在 CreateA 中加载,它应该在 DestroyA 中卸载。
  • 创建一个 RAII 包装器绝不是一个丑陋的 hack。

标签: c++ dll static singleton raii


【解决方案1】:

我的回答与人们每次谈论单身时的回答都是一样的:不要这样做!

在卸载某些库之后,您的单例会导致调用析构函数太晚。如果您删除“静态”并使其成为常规对象,在更紧凑的范围内实例化,它会在任何库被卸载之前被销毁,并且一切都应该再次工作。

只是不要使用单例。

【讨论】:

  • 我和你在一起,永远不需要在我自己的代码中使用单例。
  • RAII 的重点是利用范围规则来管理您的资源。单例通过确保资源“始终”在那里(至少直到某些实现定义的事件发生,例如正在卸载的库)来回避这一点
  • 我通常不喜欢“不要那样做”的答案,但对于 C++ 来说,有时这是唯一理智的事情。 +1。
【解决方案2】:

首先,在给出的示例中,我看不到Singleton::~Singleton() 是如何访问pA 的,因为您在Singleton::getA() 中将pA 声明为静态局部变量。

然后您解释了“B”库是在A* Create(); 中动态加载的。那它在哪里卸载呢?为什么在调用 DestroyB() 之前在 void DestroyA() 中没有卸载“B”库?

什么叫“静态链接 DLL”?

最后,我不想苛刻,但肯定有更好的设计可以将单例放在任何地方:换句话说,你真的需要管理“A”生命周期的对象是单例吗?您可以在应用程序的开头调用 CreateA() 并依靠 atexit 进行 DestroyA() 调用。

编辑:既然您依赖操作系统来卸载“B”库,为什么不从DestroyA() 实现中删除DestroyB() 调用。然后在“B”库使用 RAII 卸载时调用 DestroyB()(甚至是操作系统特定的机制,例如在 Windows 下从 DllMain 调用它,并在 Linux 或 Mac 下用 __attribute__((destructor)) 标记它)。

EDIT2:显然,从您的 cmets 来看,您的程序有很多单例。如果它们是静态单例(称为 Meyers 单例),并且它们相互依赖,那么迟早会崩溃。控制破坏顺序(因此是单例生命周期)需要一些工作,请参阅 Alexandrescu 的书或 @Martin 在 cmets 中提供的链接。


参考资料(有点相关,值得一读)

Clean Code Talks - Global State and Singletons

Once Is Not Enough

Performant Singletons

Modern C++ Design, Implementing Singletons

【讨论】:

  • 你真的需要管理“A”生命周期的对象成为单例吗:是的,我需要,因为在我的 Dll 中有更多的单例在它们的析构函数中执行需要访问 A。所以我必须通过一个单例来管理 A,在 Dll 中的所有其他单例都被破坏后,它会被破坏。
  • 那么我推荐阅读 Alexei Alexandrescu 的 Modern C++ Design 书。它包含一章专门讨论实现单例的风险。单例是最讨厌的设计模式,我希望 GOF 没有引入它。
  • 所以你使用单例的原因是“我们有很多单例”?那么更好的解决方案是“摆脱所有单身人士”。您所看到的是由单例引起的问题的副作用。
  • 我不同意您上次的编辑(无法控制销毁顺序)。单子的毁灭顺序与创造顺序相反。如果你有一个单格子之间的依赖顺序,你可以强制执行它(虽然不是自动的)它需要一些工作:stackoverflow.com/questions/335369/… 注意:我同意单格顿是最被滥用的模式。但这实际上更多是一个教育问题,“计算机工程师”需要被明确教授使用(它被滥用,因为它看起来很简单)。
  • 同意我滥用语言,我应该说“需要一些工作”之类的话
【解决方案3】:

起初这看起来像是 API 决斗的问题,但实际上它只是另一个静态析构函数问题。

通常最好避免从全局或静态析构函数中做任何不重要的事情,因为您已经发现了原因,但也有其他原因。

特别是:在 Windows 上,DLL 中全局和静态对象的析构函数在特殊情况下会被调用,它们的作用是有限制的。

如果您的 DLL 与 C 运行时库 (CRT) 链接,则 CRT 提供的入口点会调用全局和静态 C++ 对象的构造函数和析构函数。因此,对 DllMain 的这些限制也适用于构造函数和析构函数以及从它们调用的任何代码。

——http://msdn.microsoft.com/en-us/library/ms682583%28VS.85%29.aspx

限制在该页面上进行了解释,但不是很好。我会尽量避免这个问题,也许是通过模仿 A 的 API(及其显式创建和销毁函数)而不是使用单例。

【讨论】:

  • 我确实发现单例和 dll 不能很好地混合
【解决方案4】:

您将看到 atexit 在 MS C 运行时中的工作方式的副作用。

这是顺序:

  • 为可执行文件和与可执行文件链接的静态库中定义的处理程序调用 atexit 处理程序
  • 释放所有句柄
  • 为所有 dll 中定义的处理程序调用 atexit 处理程序

因此,如果您通过句柄手动加载 B 的 dll,则此句柄将被释放,而 B 在自动加载的 dll 中的 A 的析构函数中将不可用。

过去我在清理单例中的关键部分、套接字和数据库连接时遇到过类似的问题。

解决方案是不使用单例,或者如果必须,在主退出之前清理它们。例如

int main(int argc, char * argv [])
{
    A* a = Singleton::GetA();

    // Do stuff;

   Singleton::cleanup();
   return 0;
}

或者在你之后使用 RIIA 进行清理会更好

int main(int argc, char * argv [])
{
    Singleton::Autoclean singletoncleaner;  // cleans up singleton when it goes out of scope.

    A* a = Singleton::GetA();

    // Do stuff;

   Singleton::cleanup();
   return 0;
}

由于 Singletons 的问题,我试图完全避免它们,尤其是因为它们使测试成为地狱。如果我需要在任何地方访问同一个对象并且不想传递引用,我将它构造为 main 中的实例,然后构造函数将自己注册为实例,并且您可以像在其他任何地方一样访问它。唯一必须知道它不是单例的地方是 main。

Class FauxSingleton
{
public:
    FauxSingleton() {
        // Some construction
        if (theInstance == 0) {
            theInstance = this;
        } else {
            // could throw an exception here if it makes sense.
            // I generaly don't as I might use two instances in a unit test
        }
    }

    ~FauxSingleton() {
        if (theInstance == this) {
            theInstance = 0;
        }
    }

    static FauxSingleton * instance() {
        return theInstance;
    }

    static FauxSingleton * theInstance;
};


int main(int argc, char * argv [])
{
    FauxSingleton fauxSingleton;

    // Do stuff;
}

// Somewhere else in the application;
void foo() 
{
   FauxSingleton faux = FauxSingleton::instance();
   // Do stuff with faux;
}

显然构造函数和析构函数不是线程安全的,但通常在产生任何线程之前在 main 中调用它们。这在 CORBA 应用程序中非常有用,在这些应用程序的整个生命周期中都需要一个球体,并且还需要在许多不相关的地方访问它。

【讨论】:

    【解决方案5】:

    简单的答案是不要编写自己的代码来动态加载/卸载 DLL。
    使用处理所有细节的框架。

    一旦加载了 DLL,手动卸载它通常是个坏主意。
    有很多陷阱(如您所见)。
    最简单的解决方案是永远不要卸载 DLL(一旦加载)。让该操作系统在应用程序退出时执行此操作。

    【讨论】:

    • B Dll 未手动卸载。卸载 My Dll 时会调用单例(静态对象)的析构函数。所以这是操作系统卸载所有 Dll 的时间。现在,据我所知,DLL 的卸载顺序与加载顺序相反。所以由于 Dll B 是最后一个被加载(动态加载)的,它会先被卸载,然后再卸载 My Dll 和 A Dll。
    • 我认为您的描述是对操作系统在启动/关闭时如何自动加载 DLL 的准确描述,但我不相信当您手动执行时它是如何工作的。每次加载 DLL 时都有一个特定的调用递增计数。另一个在请求卸载时减少计数。当计数达到 0 时,DLL 被卸载。但是(我相信(让事情变得困难)):还有其他(较低级别的 APIS)调用可以加载/卸载 DLL 而无需增加/减少计数。
    猜你喜欢
    • 1970-01-01
    • 2018-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-01
    • 2020-03-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多