C++ 无锁滑动擦除
我一直在尝试实现一个无锁滑动擦除操作,但我显然遇到了问题。不幸的是,我真的需要它 为了解决与ABA cmpxchg相关的常见问题,我编写了标记的ptr“智能指针”类,该类将计数器嵌入到存储在std::atomic中的指针中。每次通过列表中的CAS更新指针时,标记值都会增加: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可以轻松实
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