17

我已经在 C++ 中实现了一个单例(静态版本)。我知道关于这种模式和潜在线程安全问题的所有争议,但我很好奇为什么这个确切的实现不会停止。程序永远不会退出,最后它仍然处于死锁状态。

单例.h:

#pragma once
#include <thread>
#include <atomic>

class Singleton
{
public:
    static Singleton& getInstance();

private:
    std::thread mThread;
    std::atomic_bool mRun;

    Singleton();
    ~Singleton();
    void threadFoo();
};

单例.cpp

#include "singleton.h"

Singleton& Singleton::getInstance()
{
    static Singleton instance;
    return instance;
} 

Singleton::Singleton()
{
    mRun.store(true);
    mThread = std::thread(&Singleton::threadFoo, this);
}

Singleton::~Singleton()
{
    mRun.store(false);

    if(mThread.joinable())
        mThread.join();
}

void Singleton::threadFoo()
{
    while(mRun)
    {
    }
}

主文件

#include "singleton.h"

int main()
{
    Singleton::getInstance();
    return 0;
}

我已经知道的:

  • 线程终止
  • 主线程卡在join中
  • 它与静态有关,如果我将构造函数公开并在 main() 中创建 Singleton 的实例,它将正确终止。

使用 Visual Studio 2012。感谢您的建议。

4

6 回答 6

22

在主线程上,main()终止后,CRT 获取退出锁并调用您的静态实例析构函数,该析构函数等待您的后台线程退出。

在后台线程上,您的线程函数终止后,CRT 会尝试获取退出锁以执行一些线程终止工作。这将永远阻塞,因为退出锁由等待线程退出的主线程持有。

这是一个由 CRT 实现引起的简单死锁。底线是您不能在 Windows 上的静态实例析构函数中等待线程终止。

于 2013-06-13T18:48:04.897 回答
7

我已经追查到void __cdecl _lock(int locknum)里面了mlock.c。结束时main(),主线程去那里并进入临界区EnterCriticalSection( _locktable[locknum].lock );。然后调用 Singleton 析构函数,另一个线程尝试进入相同的临界区,但不能,因此它开始等待主线程离开临界区。反过来,主线程等待另一个线程。所以我想这是一个错误。

于 2013-06-13T18:47:09.627 回答
4

好的,谢谢大家的提示。显然,这种模式实现会导致 VC++ 出现死锁。

在做了一些进一步的研究之后,我发现这个实现基于在 VC++ 中工作的 C++11 机制。

单例.h

#pragma once
#include <thread>
#include <atomic>
#include <memory>
#include <mutex>


class Singleton
{
public:
    static Singleton& getInstance();
    virtual ~Singleton();

private:
    static std::unique_ptr<Singleton> mInstance;
    static std::once_flag mOnceFlag;
    std::thread mThread;
    std::atomic_bool mRun;

    Singleton();

    void threadFoo();
};

单例.cpp

#include "singleton.h"

std::unique_ptr<Singleton> Singleton::mInstance = nullptr;
std::once_flag Singleton::mOnceFlag;


Singleton& Singleton::getInstance()
{
    std::call_once(mOnceFlag, [] { mInstance.reset(new Singleton); });
    return *mInstance.get();
}


Singleton::Singleton()
{
    mRun.store(true);
    mThread = std::thread(&Singleton::threadFoo, this);
}

Singleton::~Singleton()
{ 
    mRun.store(false);

    if(mThread.joinable())
        mThread.join();
}

void Singleton::threadFoo()
{
    while(mRun.load())
    {
    }
}

更新

微软似乎意识到了这个问题。在 VC++ 论坛中,一位名为“dlafleur”的用户报告了这篇文章: https ://connect.microsoft.com/VisualStudio/feedback/details/747145

于 2013-06-13T20:18:52.810 回答
4

请参阅标准中的 [basic.start.term]:

如果在信号处理程序 (18.10) 中不允许使用标准库对象或函数,并且在 (1.10) 完成具有静态存储持续时间的对象销毁和执行 std::atexit 注册函数 (18.5) 之前不会发生,该程序具有未定义的行为。[注意:如果在对象销毁之前没有使用具有静态存储持续时间的对象,则程序具有未定义的行为。在调用 std::exit 或从 main 退出之前终止每个线程足以满足这些要求,但不是必需的。这些要求允许线程管理器作为静态存储持续时间对象。——尾注]

于 2013-06-13T20:51:11.227 回答
1

这个死锁错误与

使用 VS2012 RC 时,如果在 main() 退出后调用 std::thread::join() 将挂起

并且它在 Visual Studio 2013 中未修复。

于 2013-11-03T22:25:32.410 回答
1

它似乎在 Visual Studio 2015 及更高版本中已修复,至少对于这个特定示例而言。

于 2019-05-21T10:18:20.580 回答