以下单例实现数据是否免费?
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;
}
答案 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)
该实施不无种族。单例的原子存储虽然使用了释放语义,但只会与匹配的获取操作同步 - 即,已经由互斥锁保护的加载操作。
在锁定线程初始化单例之前,外部宽松载荷可能会读取非空指针。
另一方面,被锁保护的获取是多余的。它将与另一个线程上具有发布语义的任何商店同步,但此时(由于互斥锁),可能存储的唯一线程是当前线程。这个负载甚至不需要是原子的 - 没有商店可以从另一个线程发生。
答案 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传递给组(相同标志对象)中的调用。
在成功完成上述所选功能的执行之前,组中没有调用返回