C++ 无锁滑动擦除

C++ 无锁滑动擦除,c++,algorithm,containers,atomic,lock-free,C++,Algorithm,Containers,Atomic,Lock Free,我一直在尝试实现一个无锁滑动擦除操作,但我显然遇到了问题。不幸的是,我真的需要它 为了解决与ABA cmpxchg相关的常见问题,我编写了标记的ptr“智能指针”类,该类将计数器嵌入到存储在std::atomic中的指针中。每次通过列表中的CAS更新指针时,标记值都会增加: head.compare\u exchange\u-weak(old,old(newptr))将newptr存储为old中的递增标记。 这允许多编写器事务,但不能解决同时更新两个指针的问题。(例如,使用标记的ptr可以轻松实

我一直在尝试实现一个无锁滑动擦除操作,但我显然遇到了问题。不幸的是,我真的需要它

为了解决与ABA cmpxchg相关的常见问题,我编写了标记的ptr“智能指针”类,该类将计数器嵌入到存储在std::atomic中的指针中。每次通过列表中的CAS更新指针时,标记值都会增加:
head.compare\u exchange\u-weak(old,old(newptr))
newptr
存储为
old
中的递增标记。 这允许多编写器事务,但不能解决同时更新两个指针的问题。(例如,使用标记的ptr可以轻松实现堆栈)

请参阅代码。 第256行是erase()函数:

bool erase(list_node * node) {
    std::atomic<tagged_ptr<list_node>>* before;
    tagged_ptr<list_node> itr, after;
    for(;;) {
        // Find previous (or head) before-node-ptr
        before = &head;
        itr = before->load(std::memory_order_acquire);
        while(itr) {
            if(itr.get() == node) {
                break;
            } else if(itr.is_void()) {
                // Thread interfered iteration.
                before = &head;
                itr = before->load(std::memory_order_acquire);
            } else {
                // Access next ptr
                before = &itr->next;
                itr = before->load(std::memory_order_acquire);
            }
        }

        after = node->next.load(std::memory_order_acquire);
        if(after.is_void() || !itr) {
            return false;
        }

        // Point before-ptr to after. (set head or previous node's next ptr)
        if(before->compare_exchange_strong(itr, itr(after))) {
            // Set node->next to invalid ptr.
            // list iterators will see it and restart their operation.
            while(!node->next.compare_exchange_weak(after, after().set_void()))
                ;
            return true;
        }
        // If *before changed while trying to update it to after, retry search.
    }
}
bool擦除(列表节点*node){
std::原子*之前;
标记的ptr itr,在之后;
对于(;;){
//查找节点ptr之前的上一个(或头部)
before=&head;
itr=before->load(标准::内存\u顺序\u获取);
while(itr){
if(itr.get()==节点){
打破
}else if(itr.is_void()){
//线程干扰迭代。
before=&head;
itr=before->load(标准::内存\u顺序\u获取);
}否则{
//访问下一个ptr
before=&itr->next;
itr=before->load(标准::内存\u顺序\u获取);
}
}
after=node->next.load(std::memory\u order\u acquire);
if(在.is|void()| |!itr之后){
返回false;
}
//ptr之前到之后的点。(设置头或上一个节点的下一个ptr)
如果(之前->比较交换强(itr,itr(之后))){
//将节点->设置在无效ptr旁边。
//列表迭代器将看到它并重新启动其操作。
while(!node->next.compare_exchange_弱(after,after()。set_void())
;
返回true;
}
//如果尝试将*before更新为after时更改了它,请重试搜索。
}
}
在测试代码中,两个线程同时将节点推入列表,两个线程按数据搜索随机节点并尝试删除它们。 我遇到的问题是:

  • 列表以某种方式变成循环的(列表以null结尾),因此线程会永远停留在列表中迭代,永远找不到列表的结尾

我对您的
标记ptr
实施有一些疑问。 此外,我对代码的这一部分有一些疑问:

         } else if(itr.is_void()) {
                // Thread interfered iteration.
                before = &head;
                itr = before->load(std::memory_order_acquire);
假设一个线程删除了最后一个节点(列表中有一个节点,两个线程调用都删除)。剩下的线程将查询头指针,它是空的。您将使用这部分代码进入一个无限循环,因为它位于
while(itr)
循环中

这部分也不是原子的:

            // Point before-ptr to after. (set head or previous node's next ptr)
            if(before->compare_exchange_strong(itr, itr(after))) {
                // Set node->next to invalid ptr.
                // list iterators will see it and restart their operation.
                while(!node->next.compare_exchange_weak(after, after().set_void()))
                    ;
                return true;
}
如果第一个CA修改了之前的
,则您的
节点
是一个仍然指向列表的独立指针。另一个线程可以将其
before
设置为此
节点
并修改它并返回。 老实说,如果您的列表是循环的,那么调试并没有那么困难,只需在调试器下中断并遵循列表即可。你会看到当它循环时,你会发现它是如何做到的。您也可以使用valgrind进行此操作

使用“set_void()”方法将内部
ptr
设置为
0xFF..F
,很难掌握
taged_ptr
类,但是如果是“void”,则while中的布尔测试(itr)将返回true。我猜名称应该是无效的而不是无效的,如果是,它应该在bool操作符中返回false(非true)。如果itr变为“无效”(据我所知,在上面的代码中是可能的),则
while(itr)
将无限循环

例如,假设您有:

Head:A -> B -> C
然后,在一些线程删除后,你得到

Thread 2 removing C : Head:A, before = &B on first iteration, exiting the while(itr) loop since itr == C (scheduled here)
Thread 1 removing B : Head:A->C and B->C (scheduled just before line 286 of your example)
Thread 2 resume, and will modify B to B->null (line 283)
and then C->null to C->yourVoid (line 286, then it's scheduled)

Then, thread 1 update B->next to B->yourVoid (useless here for understanding the issue)
You now have A->C->yourVoid

每当在这里迭代时,您将有一个无限循环,因为当itr搜索到达C时,下一步是“无效”,从head重新开始迭代在这里不会解决任何问题,它将给出相同的结果,列表被破坏。

我不确定是否正确理解
标记的\u ptr
代码。假设您使用的是32位CPU,那么
tag_type
uint32
,因此
ptr
tag
在内存中的相同位置重叠(它们是相同的,因为填充是0字节)。如果您使用的是64位CPU,然后,
tag\u type
uint16
,而
ptr
get的低16位用于存储
标签。(我觉得这是不对的)。据我所知,在32位机器上使用低4位应该是安全的,而在64位机器上使用高16位(不使用48到64位)是否有一些代码来测试
taged_ptr
实现?请检查我是否测试了taged_ptr类,它是否正常工作。在x86_64上,我可以使用cmpxchg8b而不是cmpxchg16b(在具有48位虚拟地址空间的系统上)避开一个技巧,因为不使用地址的高16位。(我知道这有点古怪!)我已经用这个类成功地在RPi2上运行了一个无锁堆栈。如果sizeof(void*)==4,那么sizeof(tagged_ptr)==8,如果sizeof(void*)==8,那么sizeof(tagged_ptr)==8,并且标记存储在地址的未使用的高16位。好的一点是,
while(itr)
在这种情况下应该比较假,因为磁头不应该包含void ptr。(
head.load()
有效或无效,永不作废)请注意
before
ptr在代码中是
&head
&node->next
,因此如果erase()删除最后一个节点,cmpxchg将从
node->next
将null存储到
head
中。问题是出现了一些错误,列表变得循环,没有nullptr终止列表,线程卡在find()中。是的,我承认标记的_ptr::set_void()的命名有点混乱,它实际上意味着“无效”。Th