C ++ 11中的Double-Checked Lock Singleton

时间:2011-05-22 08:48:01

标签: c++ multithreading c++11 atomic

以下单例实现数据是否免费?

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

加载操作的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;
}

3 个答案:

答案 0 :(得分:27)

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

但是,要明确一个懒惰的单身人士最好使用经典的迈耶斯单身人士来实现。它在C ++ 11中保证了正确的语义。

§6.7.4

  

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

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

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

class Foo
{
public:
   static Foo& instance( void )
   {
      static Foo s_instance;
      return s_instance;
   }
};

答案 1 :(得分:20)

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

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

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

请参阅Anthony Williams' series on C++0x multithreading

答案 2 :(得分:7)

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

为简洁而简化:

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

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