【问题标题】:Lock-free programming: reordering and memory order semantics无锁编程:重新排序和内存顺序语义
【发布时间】:2017-02-28 20:25:36
【问题描述】:

我正在尝试在无锁编程中立足。在阅读了关于内存排序语义的不同解释后,我想澄清一下可能发生的重新排序。据我了解,编译器(由于编译程序时的优化)和CPU(在运行时?)可能会重新排序指令。

对于宽松语义cpp reference 提供以下示例:

// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B
// Thread 2:
r2 = x.load(memory_order_relaxed); // C 
y.store(42, memory_order_relaxed); // D

据说在 x 和 y 初始为零时,代码可以产生 r1 == r2 == 42,因为虽然 A 在线程 1 中排在 B 之前,C 在线程 2 中排在 D 之前,但没有什么可以阻止D 以 y 的修改顺序出现在 A 之前,B 以 x 的修改顺序出现在 C 之前。怎么会这样?这是否意味着 C 和 D 被重新排序,所以执行顺序是 DABC?是否允许重新排序 A 和 B?

对于获取-释放语义,有以下示例代码:

std::atomic<std::string*> ptr;
int data;

void producer()
{
    std::string* p  = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}

我想知道如果我们使用宽松的内存顺序而不是获取呢?我猜data的值可以在p2 = ptr.load(std::memory_order_relaxed)之前读取,但是p2呢?

最后,为什么在这种情况下可以使用宽松的内存顺序?

template<typename T>
class stack
{
    std::atomic<node<T>*> head;
 public:
    void push(const T& data)
    {
      node<T>* new_node = new node<T>(data);

      // put the current value of head into new_node->next
      new_node->next = head.load(std::memory_order_relaxed);

      // now make new_node the new head, but if the head
      // is no longer what's stored in new_node->next
      // (some other thread must have inserted a node just now)
      // then put that new head into new_node->next and try again
      while(!head.compare_exchange_weak(new_node->next, new_node,
                                        std::memory_order_release,
                                        std::memory_order_relaxed))
          ; // the body of the loop is empty
    }
};

我的意思是head.load(std::memory_order_relaxed)head.compare_exchange_weak(new_node-&gt;next, new_node, std::memory_order_release, std::memory_order_relaxed)

总结以上所有内容,我的问题本质上是我什么时候需要关心潜在的重新排序,什么时候不需要?

【问题讨论】:

  • 请注意,如果 int data 在没有同步保证的情况下被读取,那么 #2 将根据 C++ 标准具有未定义的行为 - 保证会在仍在写入时阻止读取线程查看它。那将是 UB 类型的数据竞赛,因为它不是 std::atomic 类型。 std::atomic&lt;int&gt; 使用 memory_order_relaxed 将避免 UB data,并且只为您留下一个有效(但不太有用)的程序,其中包含一个普通数据竞争错误。但是取消引用 ptr 后的字符串比较仍然是数据竞争,因为 std::string 不是原子/无锁容器。
  • @PeterCordes 我真的不明白为什么字符串比较会成为数据竞赛。在消费者中,我们一直等到p2 被初始化。然后我们比较字符串。由于“不要破坏单线程代码”规则,必须保留此顺序。一旦我们在p2 中得到了一些东西,我们就可以保证看到指向的数据(正如你提到的,在除 Alpha 之外的所有 CPU 上)。数据必须至少和指针一样新,因此如果指针被初始化,数据也必须已经被初始化。还是我误会了你?
  • 我的意思是,如果作者或读者使用 memory_order_relaxed,这是一场数据竞争,因为其中任何一个都允许指针读取为非 NULL,而 std::string 类中的数据仍在书面。 (请注意,对于非 Alpha CPU 的编译是否与 UB 无关。就像有符号整数溢出一样,即使在为具有非常明确的语义的 2 的补码机器编译时也是如此。)
  • “不破坏单线程代码”规则仅意味着编写它的线程无需检查即可读取它,但每个 其他 线程都可以看到重新排序。即单个线程按顺序看到自己的操作,但其他线程看到它们发生乱序。
  • “其中任何一个都将允许指针在 std::string 类中的数据仍在写入时读取为非 NULL” - 这不与加载指针的语句相矛盾吗?然后取消引用它可以保证你得到的数据至少和指针一样新?

标签: multithreading c++11 atomic lock-free memory-barriers


【解决方案1】:

对于#1,编译器可能会在从 x 加载之前向 y 发出存储(没有依赖关系),即使没有,从 x 加载也可以在 cpu/内存级别延迟。

对于#2,p2 将不为零,但 *p2 和 data 都不一定具有有意义的值。

对于#3,这个线程只有一个发布非原子存储的行为,它是一个发布

您应该始终关心重新排序,或者最好不要假设任何顺序:C++ 和硬件都不会从上到下执行代码,它们只尊重依赖关系。

【讨论】:

  • 您能否详细说明第 2 点:在哪种情况下 *p2 将毫无意义?关于第 3 点,我知道它使用发布来发布,但我的问题更多的是关于负载(new_node-&gt;next 的初始负载和compare_exchange_weak 失败时的负载)。为什么我们不需要在这里获取订单?
  • @mentalmushroom *p2 没有发布给轻松的读者线程查看。它是为任何获得的人发布的。在 #3 中,没有从内存中读取的非原子内容,因此没有什么可获取的。
  • 如果它不是为轻松的读者发布的,为什么ptr.load() 返回一个非零值?在 #3 中 new_nodenew_node-&gt;next 是非原子的,对吧?
  • @mentalmushroom p2.load 返回一个非零值,因为那是 p2 中的值。这与*p2 无关。是的,new_nodenew_node-&gt;next 是非原子的。它们不是从任何地方被读取的,没有什么可获取的。
  • 我猜,你的意思是 ptr.load() 返回了 ptr 中的内容。但根据文档,std::atomic&lt;std::string*&gt; ptr 将被零初始化,因此它应该始终返回nullptr,直到那里存储其他内容。当p 被存储时,它已经初始化为new string("hello"),据我所知,只要p 是参数(即存储调用取决于在p)。还是我错了?在#3 中唯一的非原子存储似乎在head-&gt;next,但我们没有获取它,那么为什么我们需要在这里发布呢?
猜你喜欢
  • 1970-01-01
  • 2011-02-04
  • 1970-01-01
  • 2021-12-02
  • 1970-01-01
  • 2021-04-04
  • 1970-01-01
  • 2014-12-20
  • 1970-01-01
相关资源
最近更新 更多