C++ 11中的Double-Checked Lock Singleton

Nic*_*lli 43 c++ multithreading atomic c++11

以下单例实现数据 - 竞争是免费的吗?

static std::atomic<Tp *> m_instance;
...

static Tp &
instance()
{
    if (!m_instance.load(std::memory_order_relaxed))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!m_instance.load(std::memory_order_acquire))
        {
            Tp * i = new Tp;
            m_instance.store(i, std::memory_order_release);    
        }    
    }

    return * m_instance.load(std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)

std::memory_model_acquire了负载运行的多余?是否可以通过将它们切换到进一步放宽加载和存储操作std::memory_order_relaxed?在这种情况下,获取/释放语义是否std::mutex足以保证其正确性,或者std::atomic_thread_fence(std::memory_order_release)还需要确保构造函数的内存写入在轻松存储之前发生?然而,使用栅栏相当于有商店memory_order_release吗?

编辑:感谢John的回答,我提出了以下应该是数据竞争的实现.尽管内部负载可能完全是非原子的,但我决定放弃一个宽松的负载,因为它不会影响性能.与总是具有获取存储器顺序的外部负载相比,thread_local机器提高了访问大约一个数量级的实例的性能.

static Tp &
instance()
{
    static thread_local Tp *instance;

    if (!instance && 
        !(instance = m_instance.load(std::memory_order_acquire)))
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!(instance = m_instance.load(std::memory_order_relaxed)))
        {
            instance = new Tp; 
            m_instance.store(instance, std::memory_order_release);    
        }    
    }
    return *instance;
}
Run Code Online (Sandbox Code Playgroud)

def*_*ode 27

我认为这是一个很好的问题,John Calsbeek有正确的答案.

然而,要明确一个懒惰的单身人士最好使用经典的迈耶斯单身人士.它在C++ 11中保证了正确的语义.

§6.7.4

...如果控件在初始化变量时同时进入声明,则并发执行应等待初始化完成....

Meyer的单例是首选,因为编译器可以积极地优化并发代码.如果必须保留a的语义,编译器将受到更多限制std::mutex.此外,迈耶的单身是2行,几乎不可能出错.

这是迈耶的单身人士的典型例子.简单,优雅,在c ++ 03中破碎.但在c ++ 11中简单,优雅,强大.

class Foo
{
public:
   static Foo& instance( void )
   {
      static Foo s_instance;
      return s_instance;
   }
};
Run Code Online (Sandbox Code Playgroud)

  • 我认为值得注意的是,即使在Visual Studio 2013中,MS仍然不支持此C++ 11功能 (2认同)

Joh*_*eek 20

该实施不是无竞争的.单例的原子存储虽然使用了释放语义,但只会与匹配的获取操作同步 - 即,已经由互斥锁保护的加载操作.

在锁定线程完成初始化单例之前,外部宽松加载可能会读取非空指针.

另一方面,由锁保护的获取是多余的.它将与另一个线程上具有发布语义的任何商店同步,但此时(由于互斥锁),可能存储的唯一线程是当前线程.这个负载甚至不需要是原子的 - 没有商​​店可以从另一个线程发生.

请参阅Anthony Williams关于C++ 0x多线程的系列文章.


MaH*_*uJa 7

另请参见call_once.如果您之前使用单例执行某些操作,但实际上并未将返回的对象用于任何操作,则call_once可能是更好的解决方案.对于常规单例,你可以做call_once来设置一个(全局?)变量,然后返回那个变量......

为简洁起见简化:

template< class Function, class... Args>
void call_once( std::once_flag& flag, Function&& f, Args&& args...);
Run Code Online (Sandbox Code Playgroud)
  • 正好执行一个函数的执行,作为f传递给组中的调用(相同的标志对象).

  • 在成功完成上述所选功能的执行之前,组中没有调用返回