无锁双链表的原子操作

时间:2013-10-26 17:04:45

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

我正在根据这些论文编写一个无锁双链表:

“基于引用计数的高效可靠的无锁内存回收” Anders Gidenstam,IEEE会员,Marina Papatriantafilou,H?akan Sundell和Philippas Tsigas

“无锁deques和双链表” HåkanSundell,Philippas Tsigas

对于这个问题,我们可以放下第一篇论文。

在本文中,他们使用智能方法在一个单词中存储删除标志和指针。 (更多信息here

本文中本节的伪代码:

union Link
    : word
    (p,d): {pointer to Node, boolean} 

structure Node
    value: pointer to word
    prev: union Link
    next: union Link

我上面的伪代码代码:

template< typename NodeT >
struct LockFreeLink
{
public:
    typedef NodeT NodeType;

private:

protected:
    std::atomic< NodeT* > mPointer;

public:
    bcLockFreeLink()
    {
        std::atomic_init(&mPointer, nullptr);
    }
    ~bcLockFreeLink() {}

    inline NodeType* getNode() const throw()
    {
        return std::atomic_load(&mPointer, std::memory_order_relaxed);
    }
    inline std::atomic< NodeT* >* getAtomicNode() const throw()
    {
        return &mPointer;
    }
};

struct Node : public LockFreeNode
{
    struct Link : protected LockFreeLink< Node >
    {
        static const int dMask = 1;
        static const int ptrMask = ~dMask;

        Link() { } throw()
        Link(const Node* pPointer, bcBOOL pDel = bcFALSE) throw()
        { 
            std::atomic_init(&mPointer, (reinterpret_cast<int>(pPointer) | (int)pDel)); 
        }

        Node* pointer() const throw() 
        { 
            return reinterpret_cast<Node*>(
                std::atomic_load(&data, std::memory_order_relaxed) & ptrMask); 
        }
        bool del() const throw() 
        { 
            return std::atomic_load(&data, std::memory_order_relaxed) & dMask; 
        }
        bool compareAndSwap(const Link& pExpected, const Link& pNew) throw() 
        { 
            Node* lExpected = std::atomic_load(&pExpected.mPointer, std::memory_order_relaxed);
            Node* lNew = std::atomic_load(&pNew.mPointer, std::memory_order_relaxed);

            return std::atomic_compare_exchange_strong_explicit(
                &mPointer,
                &lExpected,
                lNew,
                std::memory_order_relaxed,
                std::memory_order_relaxed); 
        }

        bool operator==(const Link& pOther) throw() 
        { 
            return std::atomic_load(data, std::memory_order_relaxed) == 
                std::atomic_load(pOther.data, std::memory_order_relaxed); 
        }
        bool operator!=(const Link& pOther) throw() 
        { 
            return !operator==(pOther); 
        }
    };

    Link mPrev;
    Link mNext;
    Type mData;

    Node() {};
    Node(const Type& pValue) : mData(pValue) {};
};

在本文中,这个函数用于将链接的设置删除标记设置为true:

procedure SetMark(link: pointer to pointer to Node)
    while true do
       node = *link;
       if node.d = true or CAS(link, node, (node.p, true)) then break;

这个函数的代码:

void _setMark(Link* pLink)
{
    while (bcTRUE)
    {
        Link lOld = *pLink;
        if(pLink->del() || pLink->compareAndSwap(lOld, Link(pLink->pointer(), bcTRUE)))
            break;
    }
}

但是我的问题出在compareAndSwap函数中,我必须比较和交换三个原子变量。有关问题的信息是here

(比较和交换函数中的new变量并不重要,因为它是线程本地的)

现在我的问题:我如何编写compareAndSwap函数来比较和交换三个原子变量或我在哪里犯错?

(请原谅我很久的问题)

编辑:

类似的问题出现在内存管理器文件中:

function CompareAndSwapRef(link:pointer to pointer toNode,
old:pointer toNode, new:pointer toNode):boolean
    if CAS(link,old,new) then
        if new=NULL then
            FAA(&new.mmref,1);
            new.mmtrace:=false;
    if old=NULLthen FAA(&old.mmref,-1);
    return true;
return false; 

这里我必须比较和交换三个原子变量。 (请注意,我的参数是Link的类型,我必须比较并交换mPointer的{​​{1}}

3 个答案:

答案 0 :(得分:2)

除非你能将你正在比较/交换的三个数据项放到两个指针大小的元素中,否则你不能用比较和交换来做(当然不能在x86上做,而且我没有听说过任何其他的有这样一件事的机器架构)。

如果您依赖于存储在(至少)与偶数字节地址对齐的地址上的数据,则可能在删除元素时使用按位OR来设置最低位。在过去,人们一直在使用地址的上半部分来存储额外的数据,但至少在x86-64中,这是不可能的,因为地址的上半部分必须是“规范的”,这意味着任何地址位在“可用限制”(由处理器体系结构定义,当前为48位)之上,必须与可用限制的最高位相同(因此,与位47相同)。

编辑:这部分代码正是我所描述的:

    static const int dMask = 1;
    static const int ptrMask = ~dMask;

    Link() { } throw()
    Link(const Node* pPointer, bcBOOL pDel = bcFALSE) throw()
    { 
        std::atomic_init(&mPointer, (reinterpret_cast<int>(pPointer) | (int)pDel)); 
    }

    Node* pointer() const throw() 
    { 
        return reinterpret_cast<Node*>(
            std::atomic_load(&data, std::memory_order_relaxed) & ptrMask); 
    }

它使用最低位来存储pDel标志。

您应该能够使用cmpxchg16b的形式(在x86上)为双链表执行此操作。在Windows系统中,这将是_InterlockedCompareExchange128。在gcc中(对于Unix类型的操作系统,例如Linux / MacOS),您需要首先从两个指针构造一个int128。如果您正在编译32位代码,则可能需要为Windows和Unix OS制作64位int。

答案 1 :(得分:2)

http://www.drdobbs.com/cpp/lock-free-code-a-false-sense-of-security/210600279

  

但是通过编写自己的无锁代码替换锁定批发是   不是答案。无锁代码有两个主要缺点。首先,它是   对解决典型问题没有广泛的用处 - 大量基础数据   结构,甚至是双链表,仍然没有已知的无锁   实现即可。提供新的或改进的无锁数据   结构仍然可以为你赢得至少一篇已发表的论文   期刊,有时是学位。

我认为使用它不够高效,但无论如何它的阅读都很有趣。

答案 2 :(得分:2)

在x64上,仅使用44位地址空间。如果你的指针对齐到8个字节,那么你只使用41位。对于64位,41x2仍然太大。有一个128位的比较和交换,虽然我不能保证它的速度。我总是尝试使用64位的。

也许您只需要多达20亿个节点。因此,您可以做的是预分配列表从中拉出的节点池。您可以通过使用原子操作来获取下一个空闲池索引来创建节点。然后,而不是next和prev作为指针,它们可以是节点池中的31位索引,并且剩下2位用于删除标志。假设您不需要20亿个节点,那么您还剩下更多的节点。唯一的缺点是你必须知道在启动时需要多少个节点,尽管你也可以重新分配节点。

我所做的是使用虚拟内存函数来保留GB的地址空间,然后将物理ram映射到该空间,因为我需要它来扩展我的池而不必重新分配。