1

我们有 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;
    }
}
4

5 回答 5

11

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

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

只是不要使用单例。

于 2009-12-30T12:36:57.560 回答
5

首先,在给出的示例中,我看不到如何Singleton::~Singleton()访问,pA因为您pASingleton::getA().

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

什么叫“静态链接 DLL”?

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

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

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


参考文献(有点相关,值得一读)

清洁代码讲座 - 全球状态和单身人士

一次是不够的

高性能单例

现代 C++ 设计,实现单例

于 2009-12-30T08:35:41.917 回答
3

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

一般来说,最好避免从全局或静态析构函数中做任何重要的事情,因为你已经发现了这个原因,但也有其他原因。

特别是:在 Windows 上,DLL 中的全局和静态对象的析构函数在特殊情况下被调用,并且它们可以做什么是有限制的。

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

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

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

于 2009-12-30T16:31:27.600 回答
2

您将看到 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 应用程序中非常有用,在这些应用程序的整个生命周期中都需要一个球体,并且还需要在许多不相关的地方访问它。

于 2009-12-30T16:59:19.867 回答
1

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

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

于 2009-12-30T08:01:01.643 回答