4

下面的 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;
}
4

7 回答 7

4

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

如果另一个线程在此时潜入并调用Instance(),你会因为你的foo指针无效而被敬酒。

为了真正的安全,您必须保护整个测试和设置机制,即使这意味着即使在构建指针之后也要使用互斥锁。换句话说(我假设scoped_lock()当它在这里超出范围时会释放锁(我对Boost没有什么经验)),比如:

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

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

换句话说,假设您拥有该控制权(您可能没有),只需在main启动其他线程之前创建每个单例的实例。然后根本不要使用互斥锁。那时你不会有线程问题,你可以使用规范的 don't-care-about-threads-at-all 版本:

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

而且,是的,这确实使您的代码对那些懒得阅读您的 API 文档但(IMNSHO)他们应该得到的一切的人来说更加危险:-)

于 2010-08-17T03:05:59.963 回答
3

为什么不保持简单?

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

    static Foo instance;
    return instance;
}

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

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

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

于 2010-08-17T03:41:05.557 回答
1

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

于 2010-08-17T03:40:54.297 回答
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;
}
于 2010-08-17T02:59:39.127 回答
0

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

Foo* p = new Foo;
foo = p;

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

于 2010-08-17T03:07:47.930 回答
0

感谢您的输入。在查阅了 Joe Duffy 的优秀书籍“Windows 上的并发编程”之后,我现在认为我应该使用下面的代码。这主要是他书中的代码,除了一些重命名和 InterlockedXXX 行。以下实现使用:

  1. temp 和“actual”指针上的volatile关键字,以防止编译器重新排序。
  2. InterlockedCompareExchangePointer以防止来自CPU的重新排序。

所以,这应该很安全(......对吗?):

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_;
};
于 2010-08-18T04:55:02.050 回答
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;
}
于 2010-08-18T05:12:59.930 回答