多线程队列原子操作

时间:2014-09-18 19:07:33

标签: c++ multithreading c++11

我正在玩std :: atomic结构并编写了这个无锁的多生产者多用户队列,我在这里附上。队列的想法基于两个堆栈 - 生产者和消费者堆栈,它们本质上是链接列表结构。列表的节点将索引保存到一个数组中,该数组包含您可以读取或写入的实际数据。

这个想法是列表的节点是互斥的,即指向节点的指针只能存在于生产者或消费者列表中。生产者将尝试从生产者列表中获取节点,从消费者列表中获取消费者,并且无论何时生产者或消费者获取指向节点的指针,它都应该在两个列表之外,以便其他人无法获得它。我正在使用std :: atomic_compare_exchange函数进行旋转,直到弹出一个节点。

问题是逻辑肯定有问题,或者操作不是原子的,因为我认为它们是因为即使有1个生产者和1个消费者,只要有足够的时间,队列就会活锁,我注意到的是如果你断言那个单元格!= cell-> m_next,断言就会被击中!所以它可能是我脸上的东西,我只是看不到它,所以我想知道是否有人可以投入。

THX

#ifndef MTQueue_h
#define MTQueue_h

#include <atomic>

template<typename Data, uint64_t queueSize>
class MTQueue
{
public:

    MTQueue() : m_produceHead(0), m_consumeHead(0)
    {
        for(int i=0; i<queueSize-1; ++i)
        {
            m_nodes[i].m_idx = i;
            m_nodes[i].m_next = &m_nodes[i+1];
        }
        m_nodes[queueSize-1].m_idx = queueSize - 1;
        m_nodes[queueSize-1].m_next = NULL;

        m_produceHead = m_nodes;
        m_consumeHead = NULL;
    }

    struct CellNode
    {
        uint64_t m_idx;
        CellNode* m_next;
    };

    bool push(const Data& data)
    {
        if(m_produceHead == NULL)
            return false;

        // Pop the producer list.
        CellNode* cell = m_produceHead;
        while(!std::atomic_compare_exchange_strong(&m_produceHead,
                                                   &cell, cell->m_next))
        {
            cell = m_produceHead;
            if(!cell)
                return false;
        }

        // At this point cell should point to a node that is not in any of the lists
        m_data[cell->m_idx] = data;

        // Push that node as the new head of the consumer list
        cell->m_next = m_consumeHead;
        while (!std::atomic_compare_exchange_strong(&m_consumeHead,
                                                    &cell->m_next, cell))
        {
            cell->m_next = m_consumeHead;
        }
        return true;
    }

    bool pop(Data& data)
    {
        if(m_consumeHead == NULL)
            return false;

        // Pop the consumer list
        CellNode* cell = m_consumeHead;
        while(!std::atomic_compare_exchange_strong(&m_consumeHead,
                                                   &cell, cell->m_next))
        {
            cell = m_consumeHead;
            if(!cell)
                return false;
        }

        // At this point cell should point to a node that is not in any of the lists
        data = m_data[cell->m_idx];

        // Push that node as the new head of the producer list
        cell->m_next = m_produceHead;
        while(!std::atomic_compare_exchange_strong(&m_produceHead,
                                                   &cell->m_next, cell))
        {
            cell->m_next = m_produceHead;
        }
        return true;
    };

private:

    Data m_data[queueSize];

    // The nodes for the two lists
    CellNode m_nodes[queueSize];

    volatile std::atomic<CellNode*> m_produceHead;
    volatile std::atomic<CellNode*> m_consumeHead;
};

#endif

2 个答案:

答案 0 :(得分:2)

我相信我能够破解这一个。对于从2到1024的队列以及从1个生产者和1个消费者到100个生产者/ 100个消费者的队列,没有1000000的活锁写入/读取。

这是解决方案。诀窍是不要在比较和交换中直接使用cell-&gt; m_next(顺便说一下,这适用于生产者代码)并要求严格的内存顺序规则:

这似乎证实了我怀疑是编译器对读写的重新排序。 这是代码:

bool push(const TData& data)
{
    CellNode* cell = m_produceHead.load(std::memory_order_acquire);
    if(cell == NULL)
        return false;

    while(!std::atomic_compare_exchange_strong_explicit(&m_produceHead,
                                                        &cell,
                                                        cell->m_next,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        if(!cell)
            return false;
    }

    m_data[cell->m_idx] = data;

    CellNode* curHead = m_consumeHead;
    cell->m_next = curHead;
    while (!std::atomic_compare_exchange_strong_explicit(&m_consumeHead,
                                                         &curHead,
                                                         cell,
                                                         std::memory_order_acquire,
                                                         std::memory_order_release))
    {
        cell->m_next = curHead;
    }

    return true;
}

bool pop(TData& data)
{
    CellNode* cell = m_consumeHead.load(std::memory_order_acquire);
    if(cell == NULL)
        return false;

    while(!std::atomic_compare_exchange_strong_explicit(&m_consumeHead,
                                                        &cell,
                                                        cell->m_next,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        if(!cell)
            return false;
    }

    data = m_data[cell->m_idx];

    CellNode* curHead = m_produceHead;
    cell->m_next = curHead;
    while(!std::atomic_compare_exchange_strong_explicit(&m_produceHead,
                                                        &curHead,
                                                        cell,
                                                        std::memory_order_acquire,
                                                        std::memory_order_release))
    {
        cell->m_next = curHead;
    }

    return true;
};

答案 1 :(得分:1)

我发现你的队列实现存在一些问题:

  1. 它不是一个队列,它是一个堆栈:推送的最新项目是弹出的第一个项目。并不是说堆栈有什么问题,但把它称为队列会让人感到困惑。实际上它是两个无锁堆栈:一个堆栈最初填充节点数组,另一个堆栈使用第一个堆栈作为空闲节点列表存储实际数据元素。

  2. CellNode::m_nextpush pop上都存在数据竞争(不出所料,因为它们都做同样的事情,即从一个堆栈弹出一个节点,将该节点推到另一个节点上。假设两个线程同时输入例如pop并且都从m_consumeHead读取相同的值。线程1比赛前方成功弹出并设置data。然后,线程1将m_produceHead的值写入cell->m_next,而线程2同时读取cell->m_next以传递给std::atomic_compare_exchange_strong_explicit。根据定义,两个线程同时进行的非原子读写cell->m_next是数据竞争。

    这就是所谓的良性&#34;在并发文献中竞争:读取过时/无效值,但永远不会被使用。如果您确信您的代码永远不需要在可能导致火爆的架构上运行,您可能会忽略它,但是为了严格遵守标准内存模型,您需要使m_next成为原子并至少使用memory_order_relaxed读取以消除数据竞争。

  3. ABA。比较交换循环的正确性基于这样的前提:在初始加载和后面的比较交换中具有相同值的原子指针(例如,m_produceHeadm_consumeHead)意味着因此,指针对象也必须保持不变。这个前提并不适用于任何可以比某些线程通过其比较交换循环更快地回收对象的设计。考虑这一系列事件:

    1. 主题1输入pop并读取m_consumeHeadm_consumeHead->m_next的值,但在调用比较交换之前会阻塞。
    2. 线程2成功地从m_consumeHead弹出该节点并阻止。
    3. 线程3将多个节点推送到m_consumeHead
    4. 线程2取消阻止并将原始节点推送到m_produceHead
    5. 主题3从m_produceHead弹出该节点,然后将其推回m_consumeHead
    6. 线程1最终解除锁定并调用compare-exchange函数,该函数成功,因为m_consumeHead的值相同。它弹出节点 - 这一切都很好 - 但是将m_consumeHead设置为它在步骤1中读回的陈旧m_next值。同时线程3推送的所有节点都被泄露。