在C ++中是否有生产就绪的无锁队列或哈希实现?


80

对于C ++中的无锁队列,我已经进行了大量的搜索。我找到了一些代码和一些试验-但是没有什么我能编译的。无锁哈希也将是受欢迎的。

摘要:到目前为止,我还没有肯定的答案。没有“生产就绪”库,令人惊讶的是,现有的库都不符合STL容器的API。


4
Visual Studio 2010在<concurrent_queue.h>中包含无锁队列
Rick 2010年


2
注意,奇怪的是,术语“无锁”并不一定意味着没有锁。有关一个定义,请参见en.wikipedia.org/wiki/Non-blocking_algorithm
克里斯托弗·约翰逊

8
哇,提出了一个问题,询问如何解决具有多种解决方案的多线程编程中的一个常见但困难的问题,引起了很多讨论,并赢得了很多赞誉……然后9年后,您将其作为题外话关闭了。感谢您对StackOverflow,NathanOliver,E_net4明智的唐纳德爵士,Jean-FrançoisFabre,Machavity和gre_gor / s的
贡献

1
我想说,解决问题的人可能不理解。
Derf Skren

Answers:


40

从1.53开始,boost提供了一组无锁的数据结构,包括队列,堆栈和单一生产者/单消费者队列(即环形缓冲区)。


boost :: lockfree :: queue仅适用于POD类型,并且在大多数情况下不是很有用。我敢肯定,如果有一种方法可以提供更灵活的结构,boost会引入它。
拉曼2013年

1
@rahman哪里出了问题?如果您想传递其他任何东西,尤其是具有不透明并可能阻止副作用的对象,那么您还不了解无锁设计的全部目的。
Ichthyo 2014年

为什么boost会基于链表提供无锁队列和堆栈。但是他们不提供无锁的链表吗?
德庆

25

起点将是Herb Sutter的DDJ文章(针对单个生产者和消费者多个)。他给出的代码(从每篇文章的第二页开始在线显示)使用C ++ 0x样式atomic <T>模板类型;您可以使用Boost进程间库进行模仿。

Boost代码被埋在进程间库的深处,但是已经通读了适当的头文件(atomic.hpp),以实现我熟悉的系统上必要的比较和交换操作的实现。


1
史蒂夫,我也对Boost的原子实现感兴趣,但是它们似乎驻留在Interprocess的detail /中,并且没有记录在案。它们安全使用吗?谢谢!
金·格拉斯曼(JimGräsman),2009年

我非常了解Herb Sutter的文章-您在哪里找到来源?它们不是由DDJ发布,也不在他的网站上发布(或者我是盲人?)。
RED SOFT ADAIR,2009年

1
在这些文章的相应第二页开始,这些代码是内联的。
史蒂夫·吉勒姆

3
如果您想要真正的无锁代码,同时针对多个生产者或消费者,那么我就缺席了。Sutter的多生产者队列示例不是无锁的-有一个用于序列化生产者的锁和一个用于序列化使用者的锁。如果您能找到一个,我也会对此感兴趣。
史蒂夫·吉勒姆

1
tim.klingt.org/git?p=boost_lockfree.git中有一个boost :: lockfree项目;你可以看看。它的目标之一是提供原子基元的非:: details ::版本。
09年

17

是!

写了一个无锁队列。它具有功能™:

  • 完全无需等待(无CAS循环)
  • 超快(每秒超过一亿次入队/出队操作)
  • 使用C ++ 11 move语义
  • 根据需要增长(但仅在需要时增长)
  • 对元素进行无锁内存管理(使用预分配的连续块)
  • 独立(两个标头以及一个许可证和自述文件)
  • 在MSVC2010 +,Intel ICC 13和GCC 4.7.2下进行编译(并且应在任何C ++ 11完全兼容的编译器下工作)

可以在GitHub上以简化的BSD许可使用(可以随意分发!)。

注意事项:

  • 仅用于单生产者单用户体系结构(即两个线程)
  • 在x86(-64)上进行了彻底的测试,并且应该在ARM,PowerPC和其他CPU上工作,其中对齐的本机大小的整数以及指针加载和存储自然是原子的,但尚未在非x86 CPU上进行过现场测试(如果有人一个测试它让我知道)
  • 不知道是否侵犯了任何专利(使用后果自负,等等)。请注意,我是自己设计并实施的。

2
听起来非常好,但是需要多个生产者和/或多个消费者来利用真正的多线程。
RED SOFT ADAIR 2013年

2
@RED:取决于应用程序。我只需要一个生产者/消费者,所以这就是我建造的全部;-)
卡梅伦(Cameron)

@Cameron:好东西!您是否将队列与Facebook愚蠢的ProducerConsumerQueue进行了基准比较?我已经使用您的基准代码完成了它,它似乎大大优于您的RWQ和Dmitry的SPSC。我正在使用3.06 GHz Core 2 Duo(T9900)的OS X 10.8.3,并使用带有-O3的Clang编译了代码。我这样做是因为我目前在看单生产者/单消费者队列为我的项目之一,我认为你的候选人:)
安德烈·内维斯

@André:我现在刚刚检查过:-) Facebook的愚蠢行为从空队列中退出时比我的快,而在单线程中从非空队列中退出时则稍慢。所有其他操作的速度几乎完全相同(这是在VM上使用g ++ -O3的速度)。您要使用多大的愚蠢队列?(我用的是MAX。)Mine和Dmitry都根据需要增长,而愚蠢的东西是固定的-当然,最快的入队操作是在没有空间并且只会失败的情况下进行的。查看代码,folly似乎使用了与我相同的想法,但是没有可调整大小的内容。
卡梅伦

@André:哦,还有我忘记提及的一件事-使用我的基准代码,“ Raw empty remove”基准到目前为止进行的迭代次数最多(因为它很简单,要得到可测量的结果需要更多时间),这往往不成比例地影响最终的“平均操作次数”数字。乘数(和平坦的定时值)通常更有用。无论如何,以这些速度,如果所有这些队列实际上被用于比我愚蠢的合成基准还要多的东西,它们将足够快;-)
Cameron

15

Facebook的Folly似乎具有基于C ++ 11的无锁数据结构<atomic>

我敢说这些是目前在生产中使用的,所以我猜它们可以安全地用于其他项目。

干杯!


他们也有一个MPMC队列。他们声称是“可选地阻止”。不确定它是否建议使用,它似乎不是其常规文档的一部分。
Rusty Shackleford


10

在检查了大多数给出的答案之后,我只能声明:

答案是否定的

没有这样的事情可以直接使用。


4
100%正确。在comp.programming.threads新闻组的帮助下,我得到了相同的结果。原因之一是无锁数据结构领域是一个专利矿场。因此,即使是像Intels这样的商业图书馆都在避免这种情况。
Lothar

这是C,而不是C ++。在投反对票之前,请先阅读问题。
RED SOFT ADAIR,2010年

道歉。我注意到SO不会让我撤消我的投票,因为它觉得投票太老了。我认为SO开发人员需要做更多的工作-他们似乎正在增加越来越多的无用行为。

3
为什么这个答案获得如此多的好评。该问题可以轻松编辑。或者可以在评论中。
用户


6

我知道的最接近的东西是Windows互锁单链接列表。当然,仅Windows。


哇-就是这样。我需要一些时间来检查它(我目前无法做到),但我会回头给您。
RED SOFT ADAIR,2009年

互锁单链表是一个很棒的工具,但遗憾的是它不是FIFO。
ali_bahoo 2010年

我记得这不是一个适当的清单。您不能取消任意元素的链接。您唯一可以做的就是删除整个列表。也许从那以后它就一直在

5

如果您具有多生产者/单消费者队列/ FIFO,则可以使用SLIST或琐碎的Free Lock LIFO堆栈轻松地制作一个LockFree。您要做的是为使用者使用第二个“私有”堆栈(为简便起见,也可以将其作为SLIST进行操作,也可以选择其他任何堆栈模型)。消费者从私有堆栈中弹出项目。每当私有LIFO耗尽时,您都执行Flush而不是弹出共享的并发SLIST(抓取整个SLIST链),然后遍历Flushed列表以将项目按顺序推入私有堆栈。

适用于单生产者/单消费者和多生产者/单消费者。

但是,它不适用于多消费者的情况(单生产者或多生产者)。

而且,就哈希表而言,它们是“条带化”的理想候选者,“条带化”只是将哈希表划分为每个缓存段具有锁定的段。Java并发库就是这样做的(使用32条)。如果您拥有轻巧的读写器锁,则可以同时访问哈希表以进行同时读取,并且只有在有争议的条带上进行写操作(并且可能允许您增加哈希表)时,您才会停止。

如果自己动手,请确保将您的锁与哈希条目交织在一起,而不是将所有锁放在一个相邻的数组中,这样就不太可能出现错误共享。


感谢您的回答。我正在寻找C ++中的“生产就绪”解决方案/模板。我不想自己动手。你知道这样的实现吗?
RED SOFT ADAIR,2009年

4

我可能会晚一点。

解决方案的缺乏(被问到这个问题)主要是由于C ++中的一个重要问题(在C ++ 0x / 11之前):C ++没有并发内存模型。

现在,使用std :: atomic,您可以控制内存排序问题,并进行适当的比较和交换操作。我已经为自己编写了使用C ++ 11和Micheal的危害指标(IEEE TPDS 2004)来实现Micheal&Scott的无锁队列(PODC96)的实现,以避免早期的释放和ABA问题。它工作正常,但是实现起来又快又脏,我对实际性能不满意。代码在bitbucket上可用:LockFreeExperiment

也可以使用双字CAS在没有危险指针的情况下实现无锁队列(但是64位版本只能在使用cmpxchg16b的x86-64上实现),我在此发表了一篇博客文章(队列的未经测试的代码) :为x86 / x86-64实现通用双字比较和交换(LSE博客。)

我自己的基准向我显示,双锁队列(同样在Micheal&Scott 1996年的论文中)的性能与无锁队列一样好(我还没有达到足够的争论,以至于锁数据结构存在性能问题,但是我的板凳对于现在)和Intel TBB的并发队列似乎更好(快了两倍)(相对于FreeBSD 9下的操作系统,这是我到目前为止发现的最低限度,具体取决于操作系统,该数目是8个线程)。 i7具有4个ht核心,因此具有8个逻辑CPU)的线程,并且具有非常奇怪的行为(我的简单基准测试的执行时间从几秒变为几小时!)

遵循STL样式的无锁队列的另一个限制:在无锁队列上使用迭代器没有任何意义。


3

然后英特尔线程构建模块问世了。一时间,这很好。

PS:您正在寻找并发队列和并发哈希表



1
从严格意义上讲,我知道无锁,但是我仍然认为这可能会帮助OP解决他的问题,因为无锁只是一个实现细节。我以为他正在寻找与并发访问一起工作的集合。
爱德华·A。

无锁的东西不仅是补充细节。完全是另一回事。
arunmoezhi16年

1

据我所知,还没有公开可用的东西。实现者需要解决的一个问题是,您需要一个无锁的内存分配器,该分配器存在,尽管我现在似乎找不到链接。


对我来说,为什么没有一个内存分配器是没有道理的。只需将数据结构与内部指针一起使用即可(您知道以前的好方法,直到被容器和疯狂的技巧所困扰,甚至无法实现简单的哈希表)。
Lothar

1

以下摘自Herb Sutter的关于并发无锁队列的文章http://www.drdobbs.com/parallel/writing-a-generalized-concurrent-queue/211601363?pgno=1。我进行了一些更改,例如编译器重新排序。人们需要GCC v4.4 +来编译此代码。

#include <atomic>
#include <iostream>
using namespace std;

//compile with g++ setting -std=c++0x

#define CACHE_LINE_SIZE 64

template <typename T>
struct LowLockQueue {
private:
    struct Node {
    Node( T* val ) : value(val), next(nullptr) { }
    T* value;
    atomic<Node*> next;
    char pad[CACHE_LINE_SIZE - sizeof(T*)- sizeof(atomic<Node*>)];
    };
    char pad0[CACHE_LINE_SIZE];

// for one consumer at a time
    Node* first;

    char pad1[CACHE_LINE_SIZE
          - sizeof(Node*)];

// shared among consumers
    atomic<bool> consumerLock;

    char pad2[CACHE_LINE_SIZE
          - sizeof(atomic<bool>)];

// for one producer at a time
    Node* last;

    char pad3[CACHE_LINE_SIZE
          - sizeof(Node*)];

// shared among producers
    atomic<bool> producerLock;

    char pad4[CACHE_LINE_SIZE
          - sizeof(atomic<bool>)];

public:
    LowLockQueue() {
    first = last = new Node( nullptr );
    producerLock = consumerLock = false;
    }
    ~LowLockQueue() {
    while( first != nullptr ) {      // release the list
        Node* tmp = first;
        first = tmp->next;
        delete tmp->value;       // no-op if null
        delete tmp;
    }
    }

    void Produce( const T& t ) {
    Node* tmp = new Node( new T(t) );
    asm volatile("" ::: "memory");                            // prevent compiler reordering
    while( producerLock.exchange(true) )
        { }   // acquire exclusivity
    last->next = tmp;         // publish to consumers
    last = tmp;             // swing last forward
    producerLock = false;       // release exclusivity
    }

    bool Consume( T& result ) {
    while( consumerLock.exchange(true) )
        { }    // acquire exclusivity
    Node* theFirst = first;
    Node* theNext = first-> next;
    if( theNext != nullptr ) {   // if queue is nonempty
        T* val = theNext->value;    // take it out
        asm volatile("" ::: "memory");                            // prevent compiler reordering
        theNext->value = nullptr;  // of the Node
        first = theNext;          // swing first forward
        consumerLock = false;             // release exclusivity
        result = *val;    // now copy it back
        delete val;       // clean up the value
        delete theFirst;      // and the old dummy
        return true;      // and report success
    }
    consumerLock = false;   // release exclusivity
    return false;                  // report queue was empty
    }
};

int main(int argc, char* argv[])
{
    //Instead of this Mambo Jambo one can use pthreads in Linux to test comprehensively
LowLockQueue<int> Q;
Q.Produce(2);
Q.Produce(6);

int a;
Q.Consume(a);
cout<< a << endl;
Q.Consume(a);
cout<< a << endl;

return 0;
}

4
这不是免费的。确保它不使用os提供的锁,但是它旋转的方式(例如)“ atomic <bool> ConsumerLock”绝对是锁定行为。如果线程在持有其中一个锁的同时崩溃,则无法完成更多工作。即使是赫伯本人也这样说(我认为在该文章的第4页上)。
James Caccese 2012年


0

我写这篇文章的时间大概是在2010年,我相信在不同参考文献的帮助下。多生产者单一消费者。

template <typename T>
class MPSCLockFreeQueue 
{
private:
    struct Node 
    {
        Node( T val ) : value(val), next(NULL) { }
        T value;
        Node* next;
    };
    Node * Head;               
    __declspec(align(4)) Node * InsertionPoint;  //__declspec(align(4)) forces 32bit alignment this must be changed for 64bit when appropriate.

public:
    MPSCLockFreeQueue() 
    {
        InsertionPoint = new Node( T() );
        Head = InsertionPoint;
    }
    ~MPSCLockFreeQueue() 
    {
        // release the list
        T result;
        while( Consume(result) ) 
        {   
            //The list should be cleaned up before the destructor is called as there is no way to know whether or not to delete the value.
            //So we just do our best.
        }
    }

    void Produce( const T& t ) 
    {
        Node * node = new Node(t);
        Node * oldInsertionPoint = (Node *) InterLockedxChange((volatile void **)&InsertionPoint,node);
        oldInsertionPoint->next = node;
    }

    bool Consume( T& result ) 
    {
        if (Head->next)
        {
            Node * oldHead = Head;
            Head = Head->next;
            delete oldHead;
            result = Head->value;
            return true;
        }       
        return false;               // else report empty
    }

};
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.