【问题标题】:Best practices for move constructor of a class with mutex-locked caching具有互斥锁缓存的类的移动构造函数的最佳实践
【发布时间】:2020-07-16 00:51:53
【问题描述】:

我有时会有这样的课程:

class HasMutexLockedCache {
private:
    SomeType m_regularData;
    mutable std::mutex m_mutex;
    mutable std::optaional<T> m_cache;
    // Etc.

public:
    const m_regularData& getData() const { return m_regularData; }
    
    const T& getExpensiveResult() const {
        std::scoped_lock lock(m_mutex);
        if (!m_cache) {
            m_cache = expensiveFunction();
        }
        return m_cache;
    }
    HasMutexLockedCache(const HasMutexLockedCache& other) 
        : m_regularData(other.m_regularData) 
    {
        std::scoped_lock lock(other.m_mutex);
        // I figure we don't have to lock this->m_mutex because
        // we are in the c'tor and so nobody else could possibly
        // be messing with m_cache.
        m_cache = other.m_cache;
    }
    HasMutexLockedCache(HasMutexLockedCache&& other)
        : m_regularData(std::move(other.m_regularData))
    {
        // What here?
    }
    HasMutexLockedCache& operator=(HasMutexLockedCache&& other) {
        m_regularData = std::move(other.m_regularData);
        // Bonus points: What here? Lock both mutexes? One? 
        // Only lock this->m_mutex depending on how we 
        // document thread safety?
    }
};

我的问题:HasMutexLockedCache(HasMutexLockedCache&amp;&amp; other) 中有什么内容(HasMutexLockedCache&amp; operator=(HasMutexLockedCache&amp;&amp; other) 中也是如此?我认为我们不需要锁定other.m_mutex,因为other 是一个右值引用,我们知道没有其他人可以看到它,就像我们不必将 this-&gt;m_mutex 锁定在 c'tor 中一样。但是,我需要一些指导。这里的最佳做法是什么?我们应该锁定 other.m_mutex 吗?

【问题讨论】:

    标签: c++ move-semantics


    【解决方案1】:

    您必须记住 l-value 对象可以使用 std::move 移动,所以我们也需要锁定它们:

    HasMutexLockedCache(HasMutexLockedCache&& other) {
        std::scoped_lock lock(other.m_mutex);
        m_cache = std::move(other.m_cache);
    }
    
    HasMutexLockedCache& operator=(HasMutexLockedCache&& other) {
        std::scoped_lock lock(m_mutex, other.m_mutex);
        m_cache = std::move(other.m_cache);
        return *this;
    }
    

    【讨论】:

    • 你可能是对的。正如@Human-Compiler 指出的那样,从共享的东西转移是危险的/棘手的。假设我想拥有如果 m_cache 没有被延迟评估而是设置在 c'tor 中的安全性。在这种情况下,我认为(?)move-c'tor 不必锁定other.m_mutex,因为在非懒惰版本中移动m_cache 将是不安全的:如果m_cache 设置在c' tor,那么如果在我们移动它时另一个线程正在读取它,那么 move-c'tor 将是不安全的。我认为(?)这与在惰性评估情况下没有锁定移动 c'tor 一样不安全?
    • @Ben 老实说,我不认为我特别同意 Human-Compiler 的评估。在多线程程序中,您总是需要考虑移动语义,因为有一半的标准库是可移动的。您将经常处理可能被多个线程移出的对象,或者如果未受保护而被移出的对象。这个答案的作用是在移动时避免那些 race conditions。但是在移动之后(通过任何线程)使用对象是否有意义或定义明确取决于程序逻辑并且独立于线程。
    • @Ben 必须在移动构造函数中锁定other.m_cache,因为您无法知道other 对象正在被另一个线程更新。正如您指出的那样,另一个线程可能正在尝试从您尝试 从中移动 的对象中读取。此解决方案将防止在这种情况下出现竞争条件。
    • 但是,如果我们对安全性的定义是“就像我们预先计算好的一样安全”,那么就不是“你无法知道 other 对象是否被 [读取]另一个线程”就像UB?在任何一种情况下,我都需要另一层逻辑来确保一个线程在我们离开时没有读取other? (我将编辑我的问题以包含一个未缓存的字段。)
    • @Ben 老实说,我只是在查看内部对象的复制/移动机制。我没有意识到您向所有人提供了const T&amp;。我认为这个答案确实使惰性解决方案与构造解决方案一样安全,但在另一个线程访问该const T&amp; getExpensiveResult() 的情况下两者都不安全。我能想到的唯一解决方案是让对象的内部在外部可用,以便其他线程可以在同一个互斥锁上同步。
    【解决方案2】:

    我需要一些指导。这里有哪些最佳实践?我们应该锁定 other.m_mutex 吗?

    @Galik 的回答解释了如何为此实现移动构造函数,但是您应该考虑这对于您的抽象来说是否是一个安全且连贯的想法。

    如果一个对象包含std::mutex,通常这意味着它可能在不同的时间有并发访问,这保证了这一点。如果是这种情况,那么当面对多线程时,移动语义可能很难使用——因为你可能让线程 A 在线程 B 访问它之前移动 m_cache 的内容,导致它读取一个移动的-from state(取决于正在检查的状态,可能没有明确定义)。这些类型的错误调试起来非常复杂,而且更难重现!

    通常,如果您有这样的类型,最好在线程之间显式共享这种类型,或者通过shared_ptr 共享生命周期,或者从外部进行某种形式的同步,这样每个线程就不会破坏性地干扰一个线程另一个。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-20
      • 1970-01-01
      • 1970-01-01
      • 2020-09-26
      • 1970-01-01
      • 2017-09-13
      • 2022-11-04
      相关资源
      最近更新 更多