锁定免费链接列表插入

时间:2018-02-15 21:35:28

标签: c++ multithreading concurrency atomic lock-free

假设有一个多线程应用程序,其中单个线程将元素插入到循环链表中,而许多工作线程正在遍历此列表,从而执行实际处理。

假设节点类型与此类似:

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

这是一个合理的担忧吗?标准对此有何看法?

2 个答案:

答案 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,因为它在语义上是正确的,它不会伤害任何东西,并且它可能会阻止某些模糊的系统或未来的优化会破坏您的代码。