双重检查创建线程安全单例和无锁

时间:2011-02-13 14:53:26

标签: c++ multithreading visual-c++ singleton

我编写了以下代码,创建了我的界面管理器的单例实例。

#include <intrin.h>
#pragma intrinsic(_ReadWriteBarrier)

boost::mutex global_interface_manager_creation_mutex;
interface_manager* global_interface_manager = NULL;

interface_manager* get_global_interface_manager() {
    interface_manager* volatile temp = global_interface_manager;
    _ReadWriteBarrier();
    if (temp == NULL) {
        boost::mutex::scoped_lock(global_interface_manager_creation_mutex);

        temp = global_interface_manager;

        if (temp == NULL) {
            temp = new interface_manager();
            _ReadWriteBarrier();
            global_interface_manager = temp; 
        }
    }

    return temp;
}

但我不想使用锁和内存屏障,因此请将代码更改为:

interface_manager* get_global_interface_manager() {
    interface_manager* volatile temp = global_interface_manager;

    __assume(temp != NULL);
    if (temp == NULL) {
        temp = new interface_manager();
        if(NULL != ::InterlockedCompareExchangePointer((volatile PVOID *)&global_interface_manager, temp, NULL)) {
            delete temp;

            temp = global_interface_manager;
        }
    }

    return temp;
}

看起来这段代码效果不错,但我不确定,我真的不知道如何测试它是正确的。

4 个答案:

答案 0 :(得分:1)

我的问题是:制作线程安全的单身人员真的,真的,真的有必要吗?

单身人士是值得商榷的,但他们确实有自己的用途(我想这些讨论会远远超出主题)。

然而,线程安全单例是99.99%的不必要时间,99.99%的时间实现错误(即使 知道如何做的人它在过去证明他们错了)。所以,我认为在这种情况下“你真的需要这个”是一个有效的问题。

如果在应用程序启动时创建单例实例,例如在main()中,则只有一个线程。这可以像调用get_global_interface_manager()一样简单,或者调用yourlibrary :: init()隐式调用get()。

一旦你这样做,任何关于线程安全的担忧都是无关紧要的,因为此时强制只有一个线程。并且,保证它将起作用。没有ifs和whens。

很多(如果不是全部)库都要求你在启动时调用init函数,因此这也不是一个不常见的要求。

答案 1 :(得分:1)

您是否考虑过使用pthread_oncehttp://sourceware.org/pthreads-win32/manual/pthread_once.html

这是它的用例。

#include <stddef.h> // NULL
#include <pthread.h>

static pthread_once_t once_control = PTHREAD_ONCE_INIT;
static interface_manager* m;

static void* init_interface_manager()
{
    m = new interface_manager;
    return NULL;
}

interface_manager* get_global_interface_manager()
{
    pthread_once(&once_control, &init_interface_manager);
    return m;
}

答案 2 :(得分:0)

多线程编程的一个难点部分是99.9%的时间似乎有些工作,然后失败了。

在你的情况下,没有什么可以防止两个线程从全局指针返回NULL并且都分配新的单例。一个将被删除,但你仍然会将它作为函数的返回值传回。

我甚至无法说服自己,我自己的分析是正确的。

您可以通过返回global_interface_manager而非temp轻松修复此问题。仍然有可能创建一个你转过来并删除的interface_manager,但我认为这是你的意图。

答案 3 :(得分:0)

您可以将单例存储为原子引用。实例化后,CAS设置引用。这不能保证两个副本不会被实例化,只能保证从inst返回相同的副本。由于标准C没有原子指令,我可以在Java中显示它:

class Foo
{
    static AtomicReference<Foo> foo = new AtomicReference<>();

    public static Foo inst()
    {
        Foo atomic = foo.get();
        if(atomic != null)
            return atomic;
        else
        {
            Foo newFoo = new Foo();
            //newFoo will only be set if no other thread has set it
            foo.compareAndSet(null, newFoo);
            //if the above CAS failed, foo.get would be a different object
            return foo.get();
        }
    }
}