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

Posted

技术标签:

【中文标题】双重检查创建线程安全的单例和无锁【英文标题】:Double-checked creation of thread-safe singleton and lock-free 【发布时间】:2011-02-13 14:53:26 【问题描述】:

我编写了以下代码来创建我的接口管理器的单例实例。

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

看起来这段代码运行良好,但我不确定,我真的不知道如何测试它是否正确。

【问题讨论】:

一个“经理单身人士”,真的吗?呃... 不要使用单例,问题解决了。 这是我的求知欲。你知道我的意思。 不,不是。求知欲通常是关于具有智力价值的事物。人们通常对“我怎样才能让我的设计更糟”没有太多的求知欲。单例是一个可怕的想法,最好将你的求知欲引导到“我如何在没有单例的情况下编写代码”。 哦,还有jalf.dk/blog/2010/03/… 【参考方案1】:

我的问题是:真的、真的、真的有必要制作一个线程安全的单例吗?

单身人士值得商榷,但它们确实有其用途(我想讨论这些可能会离题很远)。

然而,threadsafe 单例在 99.99% 的情况下是不必要的,而且在 99.99% 的情况下实施错误也是如此(即使是应该知道该怎么做的人过去已经证明他们做错了)。所以,我认为在这种情况下,“你真的需要这个吗”是一个合理的担忧。

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

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

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

【讨论】:

我想说线程安全是单例的基本要求。但你是对的,它几乎总是执行错误,最好避免使用单例无论如何 正如你所说,我当然也可以在 main 函数或 DLL main 中初始化该实例。但是我想知道在第一个传统双重检查代码中删除锁和内存屏障的第二个代码是正确的。单例模式不是重点。我的好奇心是第二个代码是否是线程安全的。【参考方案2】:

您是否考虑过使用pthread_once? http://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;

【讨论】:

【参考方案3】:

多线程编程的一个困难部分是,某些东西可能看起来在 99.9% 的时间都可以工作,然后就惨遭失败。

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

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

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

【讨论】:

【参考方案4】:

您可以将单例存储为原子引用。实例化后,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();
        
    

【讨论】:

以上是关于双重检查创建线程安全的单例和无锁的主要内容,如果未能解决你的问题,请参考以下文章

单例模式双重检查(DCL)引发的多线程问题

线程安全的单例模式

双重检查锁实现单例模式的线程安全问题

C#中各版本的单例模式

双重检查锁定的单例模式和延迟初始化

DCL的单例一定是线程安全的吗