在这种情况下,'double checked locking pattern'对 std::mutex 有好处吗?

Is 'double checked locking pattern' good for std::mutex in this situation?

本文关键字:mutex std pattern 这种情况下 double checked locking      更新时间:2023-10-16

我经常遇到这种线程安全结构的设计。与以下版本1一样,一个线程可能很少调用foo1::add_data(),而另一个线程经常调用foo1::get_result()。出于优化的目的,我认为它可以使用原子来应用双重检查锁定模式 (DCLP),如版本 2 所示。对于这种情况,还有其他更好的设计吗?或者它可以改进,例如使用 std::memory_order 访问原子?

版本1

class data {};
class some_data {};
class some_result {};
class foo1
{
public:
    foo1() : m_bNeedUpdate(false) {}
    void add_data(data n)
    {
        std::lock_guard<std::mutex> lock(m_mut);
        // ... restore new data to m_SomeData
        m_bNeedUpdate = true;
    }
    some_result get_result() const
    {
        {
            std::lock_guard<std::mutex> lock(m_mut);
            if (m_bNeedUpdate)
            {
                // ... process mSomeData and update m_SomeResult
                m_bNeedUpdate = false;
            }
        }
        return m_SomeResult;
    }
private:
    mutable std::mutex  m_mut;
    mutable bool        m_bNeedUpdate;
    some_data           m_SomeData;
    mutable some_result m_SomeResult;
};

版本2

class foo2
{
public:
    foo2() : m_bNeedUpdate(false) {}
    void add_data(data n)
    {
        std::lock_guard<std::mutex> lock(m_mut);
        // ... restore new data to m_SomeData
        m_bNeedUpdate.store(true);
    }
    some_result get_result() const
    {
        if (m_bNeedUpdate.load())
        {
            std::lock_guard<std::mutex> lock(m_mut);
            if (m_bNeedUpdate.load())
            {
                // ... process mSomeData and update m_SomeResult
                m_bNeedUpdate.store(false);
            }
        }
        return m_SomeResult;
    }
private:
    mutable std::mutex          m_mut;
    mutable std::atomic<bool>   m_bNeedUpdate;
    some_data                   m_SomeData;
    mutable some_result         m_SomeResult;
};

问题是版本 2 至少不是线程安全的根据 C++11(和更早的 Posix);您正在访问一个变量,可以在没有访问权限的情况下进行修改保护。 (已知双重检查锁定模式是坏了,见http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf.)它可以通过以下方式在 C++11(或非便携式更早)中工作使用原子变量,但你写的结果是未定义的行为。

我认为通过使用

允许许多线程并行读取的"读写锁"可以实现显着的改进(在代码大小以及简单性和性能方面)。Boost 为此提供了shared_mutex,但从快速浏览来看,这篇博客文章似乎以可移植的方式实现了相同类型的锁,而无需 Boost。

你说你经常打电话给get_average,你有没有考虑过只根据你没有"见过"的数字来计算平均值?它将是 O(n) 而不是 O(n^2)。

它会像

average = (last_average * last_size + static_cast<double>(
           std::accumulate(m_vecData.begin() + last_size, m_vecData.end(), 0))) /
           m_vecData.size();

它应该给你令人满意的结果,这取决于你的载体有多大。