在初始化完成之前调用单例上的方法

Calling method on singleton before initialization finished

本文关键字:单例 方法 调用 初始化      更新时间:2023-10-16

当将旧的Visual Studio 2003项目转换为2015时,我遇到了一个问题,即应用程序在启动后立即冻结。我似乎无法从调试器中获得太多信息,因为应用程序并没有真正崩溃。

当我暂停调试器时,它总是指向同一行代码,即单例GetInstance方法中的静态变量。这使它看起来像应用程序正在等待它被初始化。该对象的构造函数正在调用一个使用相同GetInstance方法的方法,因此该实例在构造函数结束之前被使用。

代码是这样工作的:

//A.cpp
A::A()
{
    B::GetInstance()->DoSomething();
}
A* A::GetInstance()
{
    static A instance; // The debugger always points out that this line is next to be executed after pausing the application
    return &instance;
}
//B.cpp
void B::DoSomething()
{
    A::GetInstance()->DoSomethingElse();
}

我知道这段代码可以改进,有办法解决它,但我想知道为什么这段代码在Visual Studio 2003中工作得很好,而在Visual Studio 2015中却中断了。

@Lehu删除的答案基本上是正确的。它是一个循环引用,如果没有被互斥锁阻塞,它就会递归。结果如下:

  1. B::DoSomething()被称为
  2. B::DoSomething()调用A::GetInstance()
  3. A::GetInstance()调用A::A()构造static A instance。这将锁定instance创建周围的临界区,以确保可以在不中断的情况下完成作业。
  4. A::A()调用B::DoSomething();看到圆形成了吗?
  5. B::DoSomething()调用A::GetInstance()
  6. A::GetInstance()试图访问instance,但instance尚未完成构建,因此执行暂停,直到它可以。

不幸的是,步骤3不能完成,直到步骤6完成。这是一个典型的死锁:3在等待6完成,6在等待3完成。引用哈德逊大兵的话:"游戏结束了,伙计!游戏结束!"

编辑:考虑到这一点,互斥不是很正确的术语。在获取静态变量instance周围设置一个临界区更为合适。

我的MCVE演示:

struct A
{
    A();
    static A* GetInstance()
    {
        static A instance; 
        return &instance;
    }
    void DoSomethingElse()
    {
    }
};
struct B
{
    void DoSomething()
    {
        A::GetInstance()->DoSomethingElse();
    }
    static B* GetInstance()
    {
        static B instance; 
        return &instance;
    }
};
A::A()
{
    B::GetInstance()->DoSomething();
}

int main()
{
    B::GetInstance()->DoSomething();
}