我目前正在使用C ++ 11中的无锁单链表,我的popFront()
函数存在问题 - 或者我至少应该说我知道它会有在某些情况下的问题。
无论如何,这就是我目前所拥有的:
std::shared_ptr<T> popFront(void)
{
auto p = atomic_load(&head);
while(p && !atomic_compare_exchange_weak(&head, &p, p->next))
{}
return p ? p->data : std::shared_ptr<T>();
}
请注意,head
的类型为shared_ptr
。
但是,我预计会遇到一些问题。第一种情况是两个线程正在执行popFront()
,它们都读取相同的head
,并且一个线程首先完成。在第二个线程完成之前,调用者删除指向的对象,因此第二个线程正在使用已删除的内存。第二个问题是经典的ABA问题。
这个链表背后的想法是让它无锁,所以我想避免在这个函数中强加一个锁。不幸的是,我不知道如何解决这些问题。任何建议将不胜感激。
答案 0 :(得分:0)
在没有ABA问题的情况下,设计无锁队列有很多解决方案。
此article应提供一些见解,并且可以找到解决此问题的一些常用工具here。
现在,根据您提到的所述问题:
在第二个线程完成之前,调用者删除指向的对象,因此第二个线程正在使用 删除内存
是的,可能会发生并且解决方案是使用tagged pointers:在32位架构上,不使用最后2个(or more)位,因此它们可用于标记和64在比特架构中,我们至少有3个未使用的比特。
因此我们可以设置为逻辑删除指针,但不能通过设置指针的一些未使用位来物理删除它,如下所示:
__inline struct node* setTag(struct node* p, unsigned long TAG)
{
return (struct node*) ((uintptr_t)p | TAG);
}
__inline bool isTagged(struct node* p, unsigned long TAG)
{
return (uintptr_t)p == (uintptr_t)p & ~TAG;
}
__inline struct node* getUntaggedAddress(struct node* p, unsigned long TAG)
{
return (struct node*)((uintptr_t)p & ~TAG);
}
其中TAG最多为4(对于32位架构),在64位架构上最多为8(根据计算机架构和字对齐,有2/3或更多未使用的位)。
现在做CAS时,我们忽略了标记指针=&gt;因此只对有效指针进行操作。
在队列上出列队列时,我们可以按如下方式执行:
int dequeue(qroot* root)
{
qnode* oldHead;
do
{
oldHead = root->head;
if (isTagged(root->head)) //disregard tagged addresses
return NULL;
oldHead = getUntaggedAddress(root->head); //we do a CAS only if the old head was unchanged
} while (root->head.compare_exchange_strong(oldHead, oldHead->next, std::memory_order_seq_cst));
return &(oldHead->data);
}
给出
typedef struct qnode
{
std::atomic<qnode*> next;
int data;
}qnode;
typedef struct qroot
{
std::atomic<qnode*> head; //Dequeue and peek will be performed from head
std::atomic<qnode*> tail; //Enqueue will be performed to tail
}qroot;
答案 1 :(得分:0)
有助于使线程更容易的一件事就是不释放内存。如果您正在使用一堆链表节点,则可以考虑使用它们的池。您可以将其返回池中,而不是释放节点。这可以处理部分问题。
ABA很简单。每次更改头指针时都需要碰撞计数器。你需要同时用指针原子地写这个计数器。如果使用32位寻址,则使用64位比较和交换(CAS),并将计数器存储在额外的32位中。如果使用64位寻址,请避免128位比较和交换,因为它可能很慢(在我们的Xenon芯片上,任何高达64位的速度都很快)。由于Windows和Linux都不支持完整的64位寻址,因此您可以使用64位中的一些用于ABA。我使用联合为32位和64位寻址模式执行此操作。
您无需计算每项更改。你只需抓住每一个变化。即使试图让ABA有很多线程尽可能快地改变头部,它仍然很少发生。在现实生活中,它很少发生。即你不需要非常高。我通常使用4位并让它滚动。我可能会遇到麻烦。如果需要,可以使用更多。
在这个例子中,我假设64位,并使用CAS()进行比较和交换,因此您必须替换编译器用于CAS的任何内容:
typedef unsigned __int64_t U8;
struct TNode {
TNode* m_pNext;
};
template<class T>
union THead {
struct {
U8 m_nABA : 4,
m_pNode:60; // Windows only supports 44 bits addressing anyway.
};
U8 m_n64; // for CAS
// this constructor will make an atomic copy on intel
THead(THead& r) { m_n64 = r.m_n64; }
T* Node() { return (T*)m_pNode; }
// changeing Node bumps aba
void Node(T* p) { m_nABA++; m_pNode = (U8)p; return this; }
};
// pop pNode from head of list.
template<class T>
T* Pop(volatile THead<T>& Head) {
while (1) { // race loop
// Get an atomic copy of head and call it old.
THead<T> Old(Head);
if (!Old.Node())
return NULL;
// Copy old and call it new.
THead<T> New(Old);
// change New's Node, which bumps internal aba
New.Node(Old.Node()->m_pNext);
// compare and swap New with Head if it still matches Old.
if (CAS(&Head.m_n64, Old.m_n64, New.m_n64))
return Old.Node(); // success
// race, try again
}
}
// push pNode onto head of list.
template<class T>
void Push(volatile THead<T>& Head, T* pNode) {
while (1) { // race loop
// Get an atomic copy of head and call it old.
// Copy old and call it new.
THead<T> Old(Head), New(Old);
// Wire node t Head
pNode->m_pNext = New.Node();
// change New's head ptr, which bumps internal aba
New.Node(pNode);
// compare and swap New with Head if it still matches Old.
if (CAS(&Head.m_n64, Old.m_n64, New.m_n64))
break; // success
// race, try again
}
}