在C ++上进行双重检查锁定:对临时指针是新的,然后将其分配给实例

时间:2010-08-17 02:55:17

标签: c++ singleton double-checked-locking

以下Singleton实现有什么问题吗?

Foo& Instance() {
    if (foo) {
        return *foo;
    }
    else {
        scoped_lock lock(mutex);

        if (foo) {
            return *foo;
        }
        else {
            // Don't do foo = new Foo;
            // because that line *may* be a 2-step 
            // process comprising (not necessarily in order)
            // 1) allocating memory, and 
            // 2) actually constructing foo at that mem location.
            // If 1) happens before 2) and another thread
            // checks the foo pointer just before 2) happens, that 
            // thread will see that foo is non-null, and may assume 
            // that it is already pointing to a a valid object.
            //
            // So, to fix the above problem, what about doing the following?

            Foo* p = new Foo;
            foo = p; // Assuming no compiler optimisation, can pointer 
                     // assignment be safely assumed to be atomic? 
                     // If so, on compilers that you know of, are there ways to 
                     // suppress optimisation for this line so that the compiler
                     // doesn't optimise it back to foo = new Foo;?
        }
    }
    return *foo;
}

7 个答案:

答案 0 :(得分:4)

不,你甚至不能假设foo = p;是原子的。它可能会加载16位32位指针,然后在加载其余指针之前将其换出。

如果另一个帖子在此时偷偷摸摸并拨打Instance(),则会因为foo指针无效而被烘烤。

为了真正的安全性,您必须保护整个测试和设置机制,即使这意味着即使在构建指针之后也使用互斥锁。换句话说(我假设scoped_lock()会在超出范围时释放锁定(我对Boost的经验不多)),类似于:

Foo& Instance() {
    scoped_lock lock(mutex);
    if (foo != 0)
        foo = new Foo();
    return *foo;
}

如果您不想使用互斥锁(出于性能原因,可能),我过去使用的一个选项是在线程开始之前构建所有单例。

换句话说,假设您拥有该控件(您可能没有),只需在开始其他线程之前在main中创建每个单例的实例。然后根本不要使用互斥锁。那时你不会遇到线程问题,你可以使用规范的不关心线程的所有版本:

Foo& Instance() {
    if (foo != 0)
        foo = new Foo();
    return *foo;
}

并且,是的,这确实使您的代码对于无法阅读您的API文档的人更加危险,但是(IMNSHO)他们应得的一切: - )

答案 1 :(得分:3)

为什么不保持简单?

Foo& Instance()
{
    scoped_lock lock(mutex);

    static Foo instance;
    return instance;
}

编辑:在C ++ 11中,线程被引入到语言中。以下是线程安全的。该语言保证实例仅在线程安全庄园中初始化一次。

Foo& Instance()
{
    static Foo instance;
    return instance;
}

所以它的懒惰评价。它的线程安全。它非常简单。赢/赢/赢。

答案 2 :(得分:1)

这取决于您使用的线程库。如果您使用的是C ++ 0x,则可以使用原子比较和交换操作并编写障碍来保证双重检查锁定的工作原理。如果您正在使用POSIX线程或Windows线程,您可能会找到一种方法。更大的问题是为什么?事实证明,单身人士通常是不必要的。

答案 3 :(得分:0)

为什么不使用真正的互斥锁,确保只有一个线程会尝试创建foo

Foo& Instance() {
    if (!foo) {
        pthread_mutex_lock(&lock);
        if (!foo) {
            Foo *p = new Foo;
            foo = p;
        }
        pthread_mutex_unlock(&lock);
    }
    return *foo;
}

这是一个免费阅读器的测试和测试设置锁。如果您希望在非原子替换环境中保证读取安全,请使用读写器锁替换上述内容。

编辑:如果您真的想要免费读者,可以先写foo,然后再写一个标志变量fooCreated = 1。检查fooCreated != 0是安全的;如果fooCreated != 0,则初始化foo

Foo& Instance() {
    if (!fooCreated) {
        pthread_mutex_lock(&lock);
        if (!fooCreated) {
            foo = new Foo;
            fooCreated = 1;
        }
        pthread_mutex_unlock(&lock);
    }
    return *foo;
}

答案 4 :(得分:0)

c ++中的new运算符总是调用两步过程:
1.)分配与简单malloc相同的内存 2.)调用给定数据类型的构造函数

Foo* p = new Foo;
foo = p;

上面的代码会将单例创建分为3步,这甚至容易受到您尝试解决的问题的影响。

答案 5 :(得分:0)

感谢您的所有投入。在咨询了Joe Duffy的优秀书籍"Concurrent Programming on Windows"后,我现在认为我应该使用下面的代码。除了一些重命名和InterlockedXXX系列外,它主要是他书中的代码。以下实现使用:

    temp和“actual”指针上的
  1. volatile 关键字,以防止从编译器重新排序。
  2. InterlockedCompareExchangePointer 以防止重新排序 CPU
  3. 那么,这应该是非常安全的(......对吗?):

    template <typename T>
    class LazyInit {
    public:
        typedef T* (*Factory)();
        LazyInit(Factory f = 0) 
            : factory_(f)
            , singleton_(0)
        {
            ::InitializeCriticalSection(&cs_);
        }
    
        T& get() {
            if (!singleton_) {
                ::EnterCriticalSection(&cs_);
                if (!singleton_) {
                    T* volatile p = factory_();
                    // Joe uses _WriterBarrier(); then singleton_ = p;
                    // But I thought better to make singleton_ = p atomic (as I understand, 
                    // on Windows, pointer assignments are atomic ONLY if they are aligned)
                    // In addition, the MSDN docs say that InterlockedCompareExchangePointer
                    // sets up a full memory barrier.
                    ::InterlockedCompareExchangePointer((PVOID volatile*)&singleton_, p, 0);
                }
                ::LeaveCriticalSection(&cs_);
            }
            #if SUPPORT_IA64
            _ReadBarrier();
            #endif
            return *singleton_;
        }
    
        virtual ~LazyInit() {
            ::DeleteCriticalSection(&cs_);
        }
    private:
        CRITICAL_SECTION cs_;
        Factory factory_;
        T* volatile singleton_;
    };
    

答案 6 :(得分:0)

您的代码没有任何问题。在scoped_lock之后,该部分中只有一个线程,所以进入的第一个线程将初始化foo并返回,然后第二个线程(如果有的话)进入,它将立即返回,因为foo不再为null。

编辑:粘贴简化代码。

Foo& Instance() {
  if (!foo) {
    scoped_lock lock(mutex);
    // only one thread can enter here
    if (!foo)
        foo = new Foo;
  }
  return *foo;
}