共享互斥锁是否比相对较大结构的原子互斥锁更有效?

Is a shared mutex more efficient than an atomic of a relatively big struct?

本文关键字:有效 是否 相对 共享 结构      更新时间:2023-10-16

假设我有一个看起来像这样的类(实际上就是这个大小):

class K
{
public:
    long long get_x() const; // lock m_mutex in shared/read-only mode
    void update( long long w ); // lock m_mutex with a unique_lock
private:
    long long m_a;
    long long m_b;
    long long m_c;
    long long m_x;
    double m_flow_factor;
    mutable boost::shared_mutex m_mutex;
};
如您所见,这应该是线程安全的。update函数每次由一个线程调用,未知但只有一个线程(保证),但访问器可以由多个线程同时调用。

update函数正在改变所有的值,并且经常被调用(每秒100次)。正如你所猜到的,当前的实现将会锁定很多。

我正在考虑使用std::atomic来避免锁,并可能使这段代码更高效。但是,我确实需要更新函数来一起更新这些值。因此,我正在考虑这样做:

class K
{
public:
    long long get_x() const
    { return data.load().x; }
    void update( long long w )
    {
        auto data_now = data.load();
        // ... work with data_now
        data.store( data_now );
    }
private:
    struct Data {
    long long a;
    long long b;
    long long c;
    long long x;
    double flow_factor;
    };
    std::atomic<Data> data;
};

我目前对std::atomic的理解是,即使这段代码比前面的代码更可读(因为它没有到处都有锁声明),作为K::Data结构体是"大的",std::atomic将只使用一个普通的互斥锁来实现(所以它不应该比我最初的实现更快)。

我说的对吗?

对于这样一个结构体的std:atomic的任何专门化都将涉及到内部锁定,因此您将一无所获,而且现在您还将面临以前没有的加载和存储之间的数据竞争,因为在以前的版本中,整个块都有独占锁定(我猜?)。

对于shared_mutex,使用普通互斥锁与shared_mutex进行分析可能是明智的,您可能会发现普通互斥锁的性能更好(这取决于您持有锁的时间)。

shared_mutex的好处只有在锁被持有很长一段时间以供读取并且写操作很少的情况下才能看到,否则,shared_mutex所涉及的开销会抵消普通互斥锁所带来的任何收益。

std::atomic并不一定比std::mutex慢。例如,在MSVC 14.0中,std::atomic的实现。Store看起来像这样:

inline void _Atomic_copy(
volatile _Atomic_flag_t *_Flag, size_t _Size,
    volatile void *_Tgt, volatile const void *_Src,
        memory_order _Order)
{   /* atomically copy *_Src to *_Tgt with memory ordering */
_Lock_spin_lock(_Flag);
_CSTD memcpy((void *)_Tgt, (void *)_Src, _Size);
_Unlock_spin_lock(_Flag);
}
inline void _Lock_spin_lock(
volatile _Atomic_flag_t *_Flag)
{   /* spin until _Flag successfully set */
while (_ATOMIC_FLAG_TEST_AND_SET(_Flag, memory_order_acquire))
    _YIELD_PROCESSOR;
}

不能保证自旋锁会比合适的std::互斥锁更快。这取决于你到底在做什么。但是与std::mutex相比,std::atomic并不总是次优的解决方案。

无论是否使用锁实现,原子(即顺序一致性)的默认使用都相当慢,可能与互斥锁一样慢。调整update()中的内存屏障可能会带来更好的性能:

void update( long long w )
{
    auto data_now = data.load(std::memory_order_acquire);
    // ... work with data_now
    data.store(data_now, std::memory_order_release);
}

同样,如果您不需要线程之间的顺序一致性(您不同步写入器和读取器),您可以进一步优化读取器:

long long get_x() const
{ return data.load(std::memory_order_relaxed).x; }