假设有一个多线程应用程序,其中单个线程将元素插入到循环链表中,而许多工作线程正在遍历此列表,从而执行实际处理。
假设节点类型与此类似:
struct Node
{
// ...
std::atomic< Node * > next;
};
在执行插入的方法中,有以下代码段:
auto newNode = new Node( ); // (A)
newNode->next.store( previousNode->next.load( std:memory_order_relaxed ) ,
std::memory_order_relaxed ); // (B)
previousNode->next.store( newNode , std::memory_order_relaxed ); // (C)
其中previousNode
已被确定为列表中newNode
的前一个。
工作线程以类似于此的方式遍历列表:
// ...
while ( true )
{
ProcessNode( * currentNode );
currentNode = currentNode.next.load( std::memory_order_relaxed );
}
工作线程跳过刚才在行(A)中创建的节点,直到上一个节点在(C)中更新为止,没有问题。
这样的设计有什么问题吗?我担心在汇编级别,为(B)和(C)生成的代码可能是这样的:
LOAD( R1 , previousNode->next ) // (1) loads previousNode->next into register R1
WRITE( newNode->next , R1 ) // (2) writes R1 to newNode->next
WRITE( previousNode->next , newNode ) // (3) writes newNode to previousNode->next
然后一些优化可以将其重新排序为:
LOAD( R1 , previousNode->next ) // (1)
WRITE( previousNode->next , newNode ) // (3)
WRITE( newNode->next , R1 ) // (2)
这可能会破坏工作线程,因为它现在可以在newNode
成员初始化之前访问next
。
这是一个合理的担忧吗?标准对此有何看法?
答案 0 :(得分:3)
是的,这是合理的担忧。宽松的内存顺序不会强制执行围栏,它只是保证操作的原子性。代码可以由编译器重新排序,或者类似的效果,由CPU本身重新排序,或者由CPU上使用的缓存产生非常类似的效果。
您是否有任何实际理由选择宽松的订单?我实际上还没有看到该订单的任何合法用途。
答案 1 :(得分:2)
你有合理的担忧。
正如您所说,编译器可以合法地将您的商店重新订购:
auto temp = previousNode->next.load( std:memory_order_relaxed )
previousNode->next.store( newNode , std::memory_order_relaxed ); // (C)
newNode->next.store( temp, std::memory_order_relaxed ); // (B)
您现在已经在初始化其值之前插入了节点!这是否发生是错误的问题。这对编译器来说是完全合法的。
以下是一个弱有序CPU如何做同样事情的例子:
auto temp = previousNode->next.load( std:memory_order_acquire );
// previousNode->next is now hot in cache
newNode->next.store( temp, std::memory_order_release); // (B)
// Suppose newNode is in the cache, but newNode->next is a cache miss
previousNode->next.store( newNode , std::memory_order_release ); // (C)
// while waiting for cache update of newNode->next, get other work done.
// Write newNode into previousNode->next, which was pulled into the cache in the 1st line.
这不会发生在x86上,因为它有总商店订单。但是,ARM ...在您的节点初始化之前再次插入了节点。
最好坚持获得/释放。
auto temp = previousNode->next.load( std:memory_order_acquire );
newNode->next.store( temp, std::memory_order_release); // (B)
previousNode->next.store( newNode , std::memory_order_release ); // (C)
relavent 版本是C行,因为它阻止了B行之后的移动。线B对第1行具有数据依赖性,因此实际上,它不会被重新排序。但是,无论如何都要使用获取第1行并释放该行B,因为它在语义上是正确的,它不会伤害任何东西,并且它可能会阻止某些模糊的系统或未来的优化会破坏您的代码。