双重检查锁定的此修复程序有什么问题? [英] What's wrong with this fix for double checked locking?

查看:113
本文介绍了双重检查锁定的此修复程序有什么问题?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我现在看到很多文章声称,在C ++中,双重检查锁定(通常用于防止多个线程尝试初始化延迟创建的单例)被破坏了.正常的双重检查锁定代码如下:

So I've seen a lot of articles now claiming that on C++ double checked locking, commonly used to prevent multiple threads from trying to initialize a lazily created singleton, is broken. Normal double checked locking code reads like this:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!instance)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;
        }

        return *instance;
    }
};

问题显然是行分配实例-编译器可以自由分配对象,然后分配指向该对象的指针,或者将指针设置为将分配该对象的位置,然后再分配它.后一种情况打破了习惯用法-一个线程可以分配内存并分配指针,但在进入休眠状态之前不运行单例的构造函数-然后第二个线程将看到该实例不为null并尝试返回该实例,即使尚未构建.

The problem apparently is the line assigning instance -- the compiler is free to allocate the object and then assign the pointer to it, OR to set the pointer to where it will be allocated, then allocate it. The latter case breaks the idiom -- one thread may allocate the memory and assign the pointer but not run the singleton's constructor before it gets put to sleep -- then the second thread will see that the instance isn't null and try to return it, even though it hasn't been constructed yet.

看到了一个建议以使用线程本地布尔值,并检查而不是instance.像这样:

I saw a suggestion to use a thread local boolean and check that instead of instance. Something like this:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;
    static boost::thread_specific_ptr<int> _sync_check;

public:
    static singleton & instance()
    {
        static singleton* instance;

        if(!_sync_check.get())
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            // Any non-null value would work, we're really just using it as a
            // thread specific bool.
            _sync_check = reinterpret_cast<int*>(1);
        }

        return *instance;
    }
};

这样,每个线程最终都会检查实例是否已创建一次,但此后停止,这虽然会降低性能,但仍不如锁定每个调用那么糟糕.但是,如果我们仅使用局部静态布尔怎么办?:

This way each thread ends up checking if the instance has been created once, but stops after that, which entails some performance hit but still not nearly so bad as locking every call. But what if we just used a local static bool?:

class singleton {
private:
    singleton(); // private constructor so users must call instance()
    static boost::mutex _init_mutex;

public:
    static singleton & instance()
    {
        static bool sync_check = false;
        static singleton* instance;

        if(!sync_check)
        {
            boost::mutex::scoped_lock lock(_init_mutex);

            if(!instance)           
                instance = new singleton;

            sync_check = true;
        }

        return *instance;
    }
};

为什么这行不通?即使将sync_check在一个线程中分配给另一个线程时,该值仍为非零,因此为true. Dobb博士的文章声称您必须锁定,因为您将永远无法赢得战斗编译器重新排序指令.这使我认为这出于某些原因不能奏效,但我不知道为什么.如果对序列点的要求像Dobb博士的文章所让我信服的那样失败,我不明白为什么锁之后的任何代码不能重新排序为锁之前的代码.这会使C ++多线程中断.

Why wouldn't this work? Even if sync_check were to be read by one thread when it's being assigned in another the garbage value will still be nonzero and thus true. This Dr. Dobb's article claims that you have to lock because you'll never win a battle with the compiler over reordering instructions. Which makes me think this must not work for some reason, but I can't figure out why. If the requirements on sequence points are as lose as the Dr. Dobb's article makes me believe, I don't understand why any code after the lock couldn't be reordered to be before the lock. Which would make C++ multithreading broken period.

我想我可以看到编译器被允许重新指定sync_check在锁之前,因为它是一个局部变量(即使它是静态的,我们也没有返回它的引用或指针),但是这样做可以仍然可以通过使其成为静态成员(实际上是全局成员)来解决.

I guess I could see the compiler being allowed to specifically reorder sync_check to be before the lock because it's a local variable (and even though it's static we're not returning a reference or pointer to it) -- but then this could still be solved by making it a static member (effectively global) instead.

那么这行得通吗?为什么?

So will this work or won't it? Why?

推荐答案

您的修复无法解决任何问题,因为对sync_check和实例的写入可以在CPU上无序进行.例如,假设对实例的前两次调用大约在两个不同的CPU上同时发生.第一个线程将以该顺序获取锁,初始化指针并将sync_check设置为true,但是处理器可以更改写入内存的顺序.在另一个CPU上,第二个线程可以检查sync_check,看是否为true,但实例可能尚未写入内存.请参阅 Xbox 360和Microsoft Windows的无锁编程注意事项了解详情.

Your fix doesn't fix anything since the writes to sync_check and instance can be done out of order on the CPU. As an example imagine the first two calls to instance happen at approximately the same time on two different CPUs. The first thread will acquire the lock, initialize the pointer and set sync_check to true, in that order, but the processor may change the order of the writes to memory. On the other CPU then it is possible for the second thread to check sync_check, see that it is true, but instance may not yet be written to memory. See Lockless Programming Considerations for Xbox 360 and Microsoft Windows for details.

您提到的特定于线程的sync_check解决方案应该可以工作(假设您将指针初始化为0).

The thread specific sync_check solution you mention should work then (assuming you initialize your pointer to 0).

这篇关于双重检查锁定的此修复程序有什么问题?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆