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

发布于 2024-09-14 19:08:36 字数 1272 浏览 9 评论 0原文

下面的单例实现有什么问题吗?

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;
}

Anything wrong with the following Singleton implementation?

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;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

剩一世无双 2024-09-21 19:08:36

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

如果此时另一个线程潜入并调用 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)他们应得的一切:-)

No, you cannot even assume that foo = p; is atomic. It's possible that it might load 16 bits of a 32-bit pointer, then be swapped out before loading the rest.

If another thread sneaks in at that point and calls Instance(), you're toasted because your foo pointer is invalid.

For true security, you will have to protect the entire test-and-set mechanism, even though that means using mutexes even after the pointer is built. In other words (and I'm assuming that scoped_lock() will release the lock when it goes out of scope here (I have little experience with Boost)), something like:

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

If you don't want a mutex (for performance reasons, presumably), an option I've used in the past is to build all singletons before threading starts.

In other words, assuming you have that control (you may not), simply create an instance of each singleton in main before kicking off the other threads. Then don't use a mutex at all. You won't have threading problems at that point and you can just use the canonical don't-care-about-threads-at-all version:

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

And, yes, this does make your code more dangerous to people who couldn't be bothered to read your API docs but (IMNSHO) they deserve everything they get :-)

似最初 2024-09-21 19:08:36

为什么不保持简单呢?

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

    static Foo instance;
    return instance;
}

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

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

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

Why not keep it simple?

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

    static Foo instance;
    return instance;
}

Edit: In C++11 where threads is introduced into the language. The following is thread safe. The language guarantees that instance is only initialized once and in a thread safe manor.

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

So its lazily evaluated. Its thread safe. Its very simple. Win/Win/Win.

一念一轮回 2024-09-21 19:08:36

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

This depends on what threading library you're using. If you're using C++0x you can use atomic compare-and-swap operations and write barriers to guarantee that double-checked locking works. If you're working with POSIX threads or Windows threads, you can probably find a way to do it. The bigger question is why? Singletons, it turns out, are usually unnecessary.

谎言月老 2024-09-21 19:08:36

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

Foo* p = new Foo;
foo = p;

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

the new operator in c++ always invovle 2-steps process :
1.) allocating memory identical to simple malloc
2.) invoke constructor for given data type

Foo* p = new Foo;
foo = p;

the code above will make the singleton creation into 3 step, which is even vulnerable to problem you trying to solve.

爱的故事 2024-09-21 19:08:36

为什么不使用真正的互斥体来确保只有一个线程会尝试创建 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;
}

Why don't you just use a real mutex ensuring that only one thread will attempt to create foo?

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

This is a test-and-test-and-set lock with free readers. Replace the above with a reader-writer lock if you want reads to be guaranteed safe in a non-atomic-replacement environment.

edit: if you really want free readers, you can write foo first, and then write a flag variable fooCreated = 1. Checking fooCreated != 0 is safe; if fooCreated != 0, then foo is initialized.

Foo& Instance() {
    if (!fooCreated) {
        pthread_mutex_lock(&lock);
        if (!fooCreated) {
            foo = new Foo;
            fooCreated = 1;
        }
        pthread_mutex_unlock(&lock);
    }
    return *foo;
}
苹果你个爱泡泡 2024-09-21 19:08:36

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

编辑:粘贴简化的代码。

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

It has nothing wrong with your code. After the scoped_lock, there will be only one thread in that section, so the first thread that enters will initialize foo and return, and then second thread(if any) enters, it will return immediately because foo is not null anymore.

EDIT: Pasted the simplified code.

Foo& Instance() {
  if (!foo) {
    scoped_lock lock(mutex);
    // only one thread can enter here
    if (!foo)
        foo = new Foo;
  }
  return *foo;
}
爱的十字路口 2024-09-21 19:08:36

感谢您的所有意见。在查阅了 Joe Duffy 的优秀书籍后, “Windows上的并发编程”,我现在认为我应该使用下面的代码。除了一些重命名和 InterlockedXXX 行之外,大部分代码来自他的书中。以下实现

  1. 在临时指针和“实际”指针上使用:易失性关键字,以防止编译器进行重新排序。
  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_;
};

Thanks for all your input. After consulting Joe Duffy's excellent book, "Concurrent Programming on Windows", I am now thinking that I should be using the code below. It's largely the code from his book, except for some renames and the InterlockedXXX line. The following implementation uses:

  1. volatile keyword on both the temp and "actual" pointers to protect against re-ordering from the compiler.
  2. InterlockedCompareExchangePointer to protect against reordering from
    the CPU.

So, that should be pretty safe (... right?):

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_;
};
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文