不能使用互斥锁读取共享数据会导致任意陈旧数据吗?

Posted

技术标签:

【中文标题】不能使用互斥锁读取共享数据会导致任意陈旧数据吗?【英文标题】:Can not using a mutex to read shared data result in arbitrarily stale data? 【发布时间】:2013-01-22 19:48:41 【问题描述】:

假设我们有一个简单的类:

class example

    bool m_isCanceled;
    example() : m_isCanceled(false) 
public:
    void cancel()  m_isCanceled = true; 

    void doWork()
    
        for (int i = 0; i < MAX_RETRIES; ++i)
        
            // Slow
            doStuff();

            if (m_isCanceled)
            
                return;
            
        
    

如果我们在一个线程上调用example::doWork(),然后在一段时间后我们在另一个线程上调用example::cancel(),那么在第一个线程看到m_isCanceled 现在为真之前是否存在限制?

在类似的情况下,我建议我们使用互斥锁保护 m_isCanceled,但我的同事说第一个线程最多会在额外的迭代后看到更新。是这样吗?

【问题讨论】:

不,他不对——没有内存栅栏,第一个线程可能永远看到更新。 如果您使用的是 C++11,您应该将 m_isCanceled 声明为 atomic&lt;bool&gt;,如果您不是,则将其访问与互斥锁同步。 @Andy :幸运的是,对于那些使用旧工具集的人来说,Boost 1.53 将包含新的 Boost.Atomic 库。 :-] @ildjarn:很高兴听到,感谢您分享信息 :) 您的m_isCanceled 上至少需要volatile - 这应该会阻止它被缓存在循环中的寄存器中。然而,在某些架构上,这还不够,因为缓存不会在另一个内核写入变量时更新——在这种情况下,您必须使用原子类型操作。 【参考方案1】:

根本没有保证。理想情况下,您会使布尔变量原子化。如果做不到这一点,让它volatile 恰好可以在几乎所有已知的平台上工作。当然,用互斥锁保护它是可以保证的。

在实践中,无论如何它都会“发生作用”。实现通常不知道doStuff 或它调用的某个函数是否操纵m_isCancelled。因此,它将无法在这些调用中将其保存在寄存器或其他东西中。

【讨论】:

您的第二段不是假定doStuff() 不可内联吗? [我很欣赏上面的评论说“慢”,这可能意味着复杂且不可内联]。 这完全取决于doStuff 的作用。绝对有可能在从未见过变化的真实平台上编写代码。 doStuff() 实际上是对成员变量的方法调用,它执行 http 请求。它将一堆成员变量传递给调用,但不是 m_isCanceled。 @Bwmat:那么它几乎肯定会“发生作用”。其他线程可以做的任何事情,其中​​一个函数都可以做,除非实现可以内联整个事情,而实际上它不能。但是,谁知道下一代更智能的编译器(或需要更多线程同步的硬件)会做什么。如果您有原子类型,请使用它们。如果性能不重要,请使用互斥锁。如果做不到这一点,请使用volatile,但在已知volatile 足够的情况下使用它是一个依赖于平台的优化。 事实上,这是几乎在所有软件中都能看到的麻烦事。如果 doStuff() 发出一个 http 请求并等待响应,那么当调用 cancel() 时,直到 doStuff() 完成后才会发生取消。这完全违背了 cancel() 的全部目的。当网络中断并且 doStuff() 挂起直到超时期限到期时会发生什么?这个问题困扰着大多数软件!它破坏了可用性。

以上是关于不能使用互斥锁读取共享数据会导致任意陈旧数据吗?的主要内容,如果未能解决你的问题,请参考以下文章

共享锁排他锁互斥锁悲观锁乐观锁行锁表锁页面锁不可重复读丢失修改读脏数据

理解概念:共享锁排他锁互斥锁悲观锁乐观锁行锁表锁页面锁不可重复读丢失修改读脏数据

Mysql中的锁:表MDL意向锁行锁

编译器说数据不能在线程之间安全地共享,即使数据包装在互斥锁中

自旋锁代替互斥锁的实践

仅读取共享内存时的互斥锁