c ++多线程单例 - 这段代码有什么问题

时间:2012-08-25 21:58:34

标签: c++ multithreading singleton

template <typename T, typename Lock>
class Singleton
{
public:
static T * getInstance()
{
    if (obj == 0)
    {
        lock.lock();
        if (obj == 0)
        {
            obj = new T;
        }
        lock.unlock();
    }
    return obj;
}

void deleteInstance()
{
   delete obj;
   obj = 0;
}

private:
volatile static T * obj = 0;
static Lock lock; // some sort of mutex
Singleton();
~Singleton();
Singleton(const Singleton &);
Singleton & operator=(const Singleton &);
};

obj必须是不稳定的吗?如果是第一个线程,创建T实例并让缓存之前第二个线程已经加载obj为0,那么现代处理器缓存是否会失效,或者第二个线程可能使用obj的0值并创建T第二次?假设两个线程都在不同的核心中运行。

另外,请告诉我使用静态T *而不是静态T作为单例数据可能出现的任何问题。

5 个答案:

答案 0 :(得分:3)

getInstance()方法正在使用double-checked locking。双重检查锁定本质上是危险的,因为可能会引入竞争条件,具体取决于编译器如何为该行生成代码:

obj = new T;

这一行主要涉及以下三个步骤:

  1. 分配sizeof (T)个字节。
  2. 在分配的空间中构建T对象。
  3. 将指定分配空间的指针指定给obj
  4. 问题是,编译器不需要生成按顺序执行这些步骤的代码。例如,它可以分配sizeof (T)字节,然后就地分配obj然后构造T。在这种情况下,存在竞争条件,其中两个不同的线程可以构造新的T对象。此外,两个不同的线程可以尝试在同一个地方构建T对象。

    请参阅Scott Meyers和Andrei Alexandrescu的C++ and the Perils of Double-Checked Locking

    deleteInstance()而言,您可能不应该提供这样的功能,因为它允许您在其他线程仍在使用时意外地删除T对象时出错。

    编辑getInstance()的实现避免了双重检查锁定:

    static T * getInstance()
    {
        lock.lock();
        if (!obj)
        {
            try
            {
                obj = new T;
            }
            catch (...)
            {
                obj = NULL;
                lock.unlock();
                throw;
            }
        }
        lock.unlock();
        return obj;
    }
    

答案 1 :(得分:2)

getInstance没问题,也没有必要让会员volatile

deleteInstance存在问题,如果从不同的线程调用它,可能会进行双重删除(即使在检查NULL之后)。

另一个问题是deleteInstance未将obj重置为NULL,因此在删除后,getInstance将返回悬空指针,而不是创建新对象。

答案 2 :(得分:2)

这是实施单身人士的一种可怕方式(并且首先实施单身人士是一个值得怀疑的做法......)。如果您使用的是支持C ++ 11相关部分的编译器,则应使用静态局部变量:

template<typename T>
T& getInstance() {
    static T instance;
    return instance;
}

否则你应该使用Boost.Thread中的call_once

//singleton.hpp
T& getInstance();

//singleton.cpp
static T* singleton;
static boost::once_flag flag=BOOST_ONCE_INIT;

static void initialize() {
    singleton = new T();
}

T& getInstance() {
    boost::call_once(flag, initialize);
    return *singleton;
}

另一种选择是添加一个函数来显式初始化你的全局变量,然后在开始任何新线程之前调用它。

答案 3 :(得分:1)

您不需要将变量设为volatile。互斥锁定操作通常由XCHG处理器指令实现,该指令也充当内存屏障(至少在英特尔处理器上) - 它将强制从内存而不是从缓存中读取变量。

答案 4 :(得分:0)

如果你有C ++ 11,那么obj为原子,getInstance中的所有问题都会消失。

static std::atomic<T*> obj;

template <typename T, typename Lock>
std::atomic<T*> Singleton<T, Lock>::obj = 0;

deleteInstance需要更多工作:

T *tmp = 0;
obj.exchange(tmp);
delete tmp;

但是deleteInstance是危险的,因为类不知道是否有任何未完成的指针,所以调用这个函数可能会从一些无辜的线程中将对象拉出来。