C ++ Standard如何使用memory_order_acquire和memory_order_release防止自旋锁互斥锁中的死锁? [英] How C++ Standard prevents deadlock in spinlock mutex with memory_order_acquire and memory_order_release?

查看:104
本文介绍了C ++ Standard如何使用memory_order_acquire和memory_order_release防止自旋锁互斥锁中的死锁?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

TL:DR:如果互斥体实现使用获取和释放操作,则实现是否可以像通常允许的那样进行编译时重新排序,并将与独立于不同锁的两个关键部分重叠?这将导致潜在的僵局.

TL:DR: if a mutex implementation uses acquire and release operations, could an implementation do compile-time reordering like would normally be allowed and overlap two critical sections that should be independent, from different locks? This would lead to a potential deadlock.

假设互斥锁在 std :: atomic_flag :

struct mutex
{
   void lock() 
   {
       while (lock.test_and_set(std::memory_order_acquire)) 
       {
          yield_execution();
       }
   }

   void unlock()
   {
       lock.clear(std::memory_order_release);
   }

   std::atomic_flag lock; // = ATOMIC_FLAG_INIT in pre-C++20
};

到目前为止,关于使用单个这样的互斥锁,看起来还可以: std :: memory_order_release std :: memory_order_acquire 同步.

So far looks ok, regarding using single such mutex: std::memory_order_release is sychronized with std::memory_order_acquire.

此处使用 std :: memory_order_acquire / std :: memory_order_release 不应一眼就引起疑问.它们类似于cppreference示例 https://en.cppreference.com/w/cpp/atomic/atomic_flag

The use of std::memory_order_acquire/std::memory_order_release here should not raise questions at the first sight. They are similar to cppreference example https://en.cppreference.com/w/cpp/atomic/atomic_flag

现在有两个互斥锁保护着不同的变量,并且有两个线程以不同的顺序访问它们:

Now there are two mutexes guarding different variables, and two threads accessing them in different order:

mutex m1;
data  v1;

mutex m2;
data  v2;

void threadA()
{
    m1.lock();
    v1.use();
    m1.unlock();

    m2.lock();
    v2.use();
    m2.unlock();
}

void threadB()
{
    m2.lock();
    v2.use();
    m2.unlock();

    m1.lock();
    v1.use();
    m1.unlock();
}

发布操作可以在不相关的获取操作之后进行重新排序(不相关的操作==对另一个对象的后续操作),因此可以按以下方式转换执行:

Release operations can be reordered after unrelated acquire operation (unrelated operation == a later operation on a different object), so the execution could be transformed as follows:

mutex m1;
data  v1;

mutex m2;
data  v2;

void threadA()
{
    m1.lock();
    v1.use();

    m2.lock();
    m1.unlock();

    v2.use();
    m2.unlock();
}

void threadB()
{
    m2.lock();
    v2.use();

    m1.lock();
    m2.unlock();

    v1.use();
    m1.unlock();
}

所以看起来好像有一个死锁.

So it looks like there is a deadlock.

问题:

  1. Standard如何防止此类互斥?
  2. 使自旋锁互斥锁不受此问题影响的最佳方法是什么?
  3. 这篇文章顶部的未修改互斥锁在某些情况下是否可用?

(不是 C ++ 11 memory_order_acquire和memory_order_release语义?,尽管它在同一区域)

(Not a duplicate of C++11 memory_order_acquire and memory_order_release semantics?, though it is in the same area)

推荐答案

在ISO C ++标准中没有问题.它不能区分编译时与运行时的重新排序,并且代码仍然必须执行 ,就好像它以源代码顺序在C ++抽象机上运行一样.因此,尝试保留第二个锁的 m2.test_and_set(std :: memory_order_acquire)的效果在保持第一个锁的同时(即在 m1.reset 之前)对其他线程可见.),但是失败并不能阻止 m1 发行.

There's no problem in the ISO C++ standard; it doesn't distinguish compile-time vs. run-time reordering, and the code still has to execute as if it ran in source order on the C++ abstract machine. So the effects of m2.test_and_set(std::memory_order_acquire) trying to take the 2nd lock can become visible to other threads while still holding the first (i.e. before m1.reset), but failure there can't prevent m1 from ever being released.

我们唯一有问题的方法是,如果 compile-time 的重新排序将某台机器的该命令固定为asm,从而使 m2 锁重试循环具有在实际释放 m1 之前退出.

The only way we'd have a problem is if compile-time reordering nailed down that order into asm for some machine, such that the m2 lock retry loop had to exit before actually releasing m1.

此外,ISO C ++仅根据与...同步"和什么可以看到什么"来定义排序,而不是根据相对于某种新顺序的"重新"排序操作来定义.那意味着存在某种秩序.除非您使用seq_cst操作,否则甚至无法保证多个线程可以达成共识的顺序也不会存在于单独的对象中.(并且保证每个对象的修改顺序都存在.)

Also, ISO C++ only defines ordering in terms of synchronizes-with and what can see what, not in terms of re-ordering operations relative into some new order. That would imply some order existed. No such order that multiple threads can agree on is even guaranteed to exist for separate objects, unless you use seq_cst operations. (And a modification order for each object separately is guaranteed to exist.)

获取和释放操作的1向障碍模型(例如https://preshing.com/20120913/acquire-and-release-semantics )是一种思考问题的便捷方法,例如,将实际情况与x86和AArch64上的纯负载和纯存储相匹配.但是就语言咨询而言,这不是ISO C ++标准定义事物的方式.

The 1-way-barrier model of acquire and release operations (like the diagram in https://preshing.com/20120913/acquire-and-release-semantics) is a convenient way to think about things, and matches reality for pure-loads and pure-stores on x86 and AArch64 for example. But as far as language-lawyering, it's not how the ISO C++ standard defines things.

在长时间运行的循环中对 atomic 操作进行重新排序是C ++标准所允许的理论问题. P0062R1:编译器何时应优化原子?a>指出,标准的1.10p28措辞在技术上允许将存储延迟到长时间运行的循环之后:

Reordering an atomic operation across a long-running loop is a theoretical problem allowed by the C++ standard. P0062R1: When should compilers optimize atomics? points out that delaying a store until after a long-running loop is technically allowed by standard's wording of 1.10p28:

实现应确保由原子或同步操作分配的最后一个值(按修改顺序)将在有限的时间段内对所有其他线程可见.

An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.

但是潜在的无限循环会违反此规定,例如在死锁情况下不是无限的,因此编译器一定不能这样做.

这不仅仅是实现"质量问题.成功互斥锁是一个获取操作,但是您应该将重试循环视为一个单独的获取操作.任何理智的编译器都不会.

It's not "just" a quality-of-implementation issue. A successful mutex lock is an acquire operation, but you should not look at the retry loop as a single acquire operation. Any sane compiler won't.

(激进的原子优化可能会破坏的经典示例是进度条,其中编译器将所有宽松的存储区循环出一个循环,然后将所有无效存储区折叠到一个100%的最终存储区中.另请参见此问题与解答-当前的编译器不接受,并且基本上将 atomic 用作 volatile atomic ,直到C ++解决了为程序员提供一种让编译器知道何时可以/不能安全优化原子的方法的问题.)

(The classic example of something that aggressive atomics optimization could break is a progress bar, where the compiler sinks all the relaxed stores out of a loop and then folds all the dead stores into one final store of 100%. See also this Q&A - current compilers don't, and basically treat atomic as volatile atomic until C++ solves the problem of giving programmers a way to let the compiler know when atomics can/can't be optimized safely.)

这篇关于C ++ Standard如何使用memory_order_acquire和memory_order_release防止自旋锁互斥锁中的死锁?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆