`
shixiaomu
  • 浏览: 382817 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

一种高效无锁内存队列的实现

    博客分类:
  • java
 
阅读更多


Disruptor是LMAX公司开源的一个高效的内存无锁队列。这两天看了一下相关的设计文档和博客,下面尝试进行一下总结。

第一部分。引子
谈到并发程序设计,有几个概念是避免不了的。

1.锁:锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能有很大的损失。用户态的锁虽然避免了这些问题,但是其实它们只是在没有真实的竞争时才有效。下面是一个计数实验中不加锁、使用锁、使用CAS及定义volatile变量之间的性能对比。

2. CAS: CAS的涵义不多介绍了。使用CAS时不像上锁那样需要一次上下文切换,但是也需要处理器锁住它的指令流水线来保证原子性,并且还要加上Memory Barrier来保证其结果可见。

3. Memory Barrier: 大家都知道现代CPU是乱序执行的,也就是程序顺序与实际的执行顺序很可能是不一致的。在单线程执行时这不是个问题,但是在多线程环境下这种乱序就可能会对执行结果产生很大的影响了。memory barrier提供了一种控制程序执行顺序的手段, 关于其更多介绍,可以参考 http://en.wikipedia.org/wiki/Memory_barrier

4. Cache Line:cache line解释起来其实很简单,就是CPU在做缓存的时候有个最小缓存单元,在同一个单元内的数据被同时被加载到缓存中,充分利用 cache line可以大大降低数据读写的延迟,错误利用cache line也会导致缓存不同替换,反复失效。

好,接下来谈一谈设计并发内存队列时需要考虑的问题。一就是数据结构的问题,是选用定长的数组还是可变的链表,二是并发控问题,是使用锁还是CAS操作,是使用粗粒度的一把锁还是将队列的头、尾、和容量三个变量分开控制,即使分开,能不能避免它们落入同一个Cache line中呢。
我们再回过头来思考一下队列的使用场景。通常我们的处理会形成一条流水线或者图结构,队列被用来作为这些流程中间的衔接表示它们之间的依赖关系,同时起到一个缓冲的作用。但是使用队列并不是没有代价的,实际上数据的入队和出队都是很耗时的,尤其在性能要求极高的场景中,这种消耗更显得奢侈。如果这种依赖能够不通过在各个流程之间放一个队列来表示那就好啦!
第二部分 正文
现在开始来介绍我们的Disruptor啦,有了前面这么多的铺垫,我想可以直入主题了。接下来我们就从队列的三种基本问题来细细分析下disruptor吧。

1.列队中的元素如何存储?
Disruptor的中心数据结构是一个基于定长数组的环形队列,如图1。
在数组创建时可以预先分配好空间,插入新元素时只要将新元素数据拷贝到已经分配好的内存中即可。对数组的元素访问对CPU cache 是非常友好的。关于数组的大小选择有一个讲究,大家都知道环形队列中会用到取余操作, 在大部分处理器上,取余操作并不高效。因此可以将数组大小设定为2的指数倍,这样计算余数只需要通过位操作 index & ( size -1 )就能够得到实际的index。
Disruptor对外只有一个变量,那就是队尾元素的下标:cursor,这也避免了对head/tail这两个变量的操作和协同。生产者和消费者对disruptor的访问分别需要通过producer barrier和consumer barrier来协调。关于这两个barrier是啥,后面会介绍。
ring buffer
图1. RingBuffer,当前的队尾元素位置为18

2.生产者如何向队列中插入元素?
生产者插入元素分为两个步骤,第一步申请一个空的slot, 每个slot只会被一个生产者占用,申请到空的slot的生产者将新元素的数据拷贝到该slot;第二步是发布,发布之后,新元素才能为消费者所见。如果只有一个生产者,第一步申请操作无需同步即可完成。如果有多个生产者,那么会有一个变量:claimSequence来记录申请位置,申请操作需要通过CAS来同步,例如图二中,如果两个生产者都想申请第19号slot, 则它们会同时执行CAS(&claimSequence, 18, 19),执行成功的人得到该slot,另一个则需要继续申请下一个可用的slot。在disruptor中,发布成功的顺序与申请的顺序是严格保持一致的,在实现上,发布事件实际上就是修改cursor的值,操作等价于CAS(&cursor, myslot-1, myslot),从此操作也可以看出,发布执行成功的顺序必定是slot, slot 1, slot 2 ….严格有序的。另外,为了防止生产者生产过快,在环形队列中覆盖消费者的数据,生产者要对消费者的消费情况进行跟踪,实现上就是去读取一下每个消费者当前的消费位置。例如一个环形队列的大小是8,有两个消费者的分别消费到第13和14号元素,那么生产者生产的新元素是不能超过20的。插入元素的过程图示如下:
publisher1
图2. RingBuffer当前的队尾位置序号为18.生产者提出申请。

图3. 生产者申请得到第19号位置,并且19号位置是独占的,可以写入生产元素。此时19号元素对消费者是不可见的。

图4,生产者成功写入19号位置后,将cursor修改为19,从而完成发布,之后消费者可以消费19号元素。

3.消费者如何获知有新的元素进来了?
消费者需要等待有新元素进入方能继续消费,也就是说cursor大于自己当前的消费位置。等待策略有多种。可以选择sleep wait, busy spin等等,在使用disruptor时,可以根据场景选择不同的等待策略。

4.批量
如果消费者发现cursor相比其最后的一次消费位置前进了不止一个位置,它就可以选择批量消费这区段的元素,而不是一次一个的向前推进。这种做法在提高吞吐量的同时还可以使系统的延迟更加平滑。

5.依赖图
前面也提过,在传统的系统中,通常使用队列来表示多个处理流程之间的依赖,并且一步依赖就需要多添加一个队列。在Disruptor中,由于生产者和消费者是分开考虑和控制的,因此有可能能够通过一个核心的环形队列来表示全部的依赖关系,可以大大提高吞吐,降低延迟。当然,要达到这个目的,还需要用户细心地去设计。下面举一个简单的例子来说明如何使用disruptor来表示依赖关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

/**
* 场景描述:生产者p1生产出来的数据需要经过消费者ep1和ep2的处理,然后传递给消费者ep3
*
*            -----
*     ----->| EP1 |------
*    |       -----       |
*    |                   v
*  ----                -----
* | P1 |              | EP3 |
*  ----                -----
*    |                   ^
*    |       -----       |
*     ----->| EP2 |------
*            -----
*
*
* 基于队列的解决方案
* ============
*                 take       put
*     put    ====      -----      ====   take
*     ----->| Q1 |<---| EP1 |--->| Q3 |<------
*    |       ====      -----      ====        |
*    |                                        |
*  ----      ====      -----      ====      -----
* | P1 |--->| Q2 |<---| EP2 |--->| Q4 |<---| EP3 |
*  ----      ====      -----      ====      -----
*
* 使用Disruptor的解决方案:
* 以一个RingBuffer为中心,生产者p1生产事件写到ringbuffer中,
* 消费者ep1和ep2仅需要根据队尾位置来进行判断是否有可消费事件即可,
* 消费者ep3则需要根据消费者ep1和ep2的位置来判断是否有可消费事件。生产者需要跟踪ep3的位置,防止覆盖未消费事件。
* ==========
*                    track to prevent wrap
*               -------------------------------
*              |                               |
*              |                               v
*  ----      ====                 =====      -----
* | P1 |--->| RB |<--------------| SB2 |<---| EP3 |
*  ----      ====                 =====      -----
*      claim   ^  get               |   waitFor
*              |                    |
*            =====      -----       |
*           | SB1 |<---| EP1 |<-----
*            =====      -----       |
*              ^                    |
*              |        -----       |
*               -------| EP2 |<-----
*             waitFor   -----
*/

第三部分 结束语
disruptor本身是用java写的,但是笔者认为在c 中更能体现其优点,自己也山寨了一个c 版本。在一个生产者和一个消费者的场景中测试表明,无锁队列相比有锁队列,qps有大约10倍的提升,latency更是有几百倍的提升。不管怎么样,现在大家都渐渐都这么一个意识了:锁是性能杀手。所以这些无锁的数据结构和算法,可以尝试借鉴来使用在合适的场景中。
分享到:
评论

相关推荐

    一个c++11实现的无锁队列.zip

    无锁队列是一种高效、线程安全的数据结构,尤其在多线程环境下,它通过避免锁的使用来提高并发性能。C++11引入了原子操作(atomic operations)和线程支持库,使得无锁编程成为可能。在这个“一个c++11实现的无锁...

    无锁消息队列实现-课件1

    无锁消息队列是一种在多线程环境下用于高效通信的数据结构,它的主要目的是避免在并发操作中使用锁带来的性能损失...在实际项目中,选择适合的无锁队列实现并进行性能测试,可以帮助我们构建高效、低延迟的多线程系统。

    cpp-一个快速多生产者多消费者的C11无锁并发队列

    无锁编程是一种编程范式,它避免了在共享数据上使用锁,而是通过原子操作(如CAS - Compare and Swap)来保证数据的一致性。C++11引入了对原子操作和线程支持的原生支持,使得实现这样的无锁数据结构成为可能。 在...

    c++11无锁队列的一种简单实现.pptx

    C++11无锁队列是一种在多线程环境中用于高效数据交换的技术,它避免了传统互斥锁带来的开销,从而提高了并发性能。在C++11中,无锁队列的实现主要依赖于新标准引入的多线程支持、原子操作和内存模型。 首先,我们来...

    Linux内核中的无锁队列 - kfifo

    在Linux内核中,`kfifo`(Kernel FIFO)是一种高效的无锁队列数据结构,它被设计为简单、优雅且性能卓越。`kfifo`的核心优势在于其在特定场景下能够避免锁的使用,从而提高系统的整体性能。在只有一个读线程和一个写...

    基于cas的无锁队列实现

    无锁队列是一种在多线程环境下实现高效并发操作的数据结构。它利用了硬件原子操作,如Compare and Swap (CAS)指令,来避免在更新数据时出现锁竞争,从而提高系统的并行性能。在这个场景中,我们将深入探讨如何在C++...

    无锁缓冲队列

    无锁缓冲队列是一种在多线程环境下高效且并发友好的数据结构,它在计算机科学和编程领域,尤其是在并发编程和高性能计算中占有重要地位。无锁缓冲队列(也称为原子队列或lock-free queue)的设计目标是通过避免锁的...

    无锁队列英文论文(具有研究价值).zip

    无锁队列是一种在多线程环境下实现高效并发的数据结构,它通过避免使用传统的互斥锁来提升系统性能。在高并发场景下,无锁队列的效率往往优于使用锁的队列,因为锁会导致线程上下文切换,增加系统的开销。这篇“无锁...

    解析C++无锁队列的实现代码

    无锁队列是一种高效的数据结构,它在多线程环境下避免了锁的使用,从而减少了线程间的同步开销,提升了并发性能。在C++中,实现无锁队列的关键在于利用原子操作(Atomic Operations)来保证数据在并发环境中的正确性...

    多线程并发访问无锁队列的算法研究.pdf

    本文将探讨一种通过动态内存分配实现的可线性化的多线程访问无锁队列算法——FIFO队列。其中,Michael and Scott提出的使用动态内存分配实现的并发无锁FIFO队列算法被认为是当前最高效和实用的算法之一,通常被称为...

    锁与原子操作CAS以及无锁队列的底层实现相关资源

    CAS(Compare and Swap,比较并交换)是一种无锁编程的重要原语,而无锁队列则是利用这些原语构建高效并发数据结构的典范。下面我们将深入探讨这些概念及其在实际应用中的底层实现。 首先,我们来看“锁”。在多...

    基于无锁数据结构的FIFO队列算法.pdf

    这篇文章介绍了一种基于无锁数据结构的FIFO(先进先出)队列算法,该算法被设计为一种高效的核间通信机制。 无锁数据结构的关键在于,它通过原子操作或者特定的算法设计,确保数据结构在并发读写时仍然保持正确性和...

    无锁队列详细分解 — 顶层设计.pdf

    首先,无锁队列是一种在多核处理器架构下优化数据结构访问和避免锁竞争的技术,它在并行计算和多线程环境中尤其有用。无锁队列可以有效提升数据处理的速度,特别是在网络包处理、任务调度等需要高并发和低延迟处理的...

    freelockqueue(多线程不需要加锁的队列性能很高)

    总的来说,"freelockqueue"为C#开发者提供了一个高效、无锁的队列实现,它能够在保证线程安全的同时,显著提升多线程环境下的性能。通过理解和应用这种技术,开发者可以优化他们的代码,使其在并发场景下运行得更加...

    无锁单生产者-单消费者循环队列

    文件列表中的"Lock-Free-Single-Producer-Single-Consumer-Circular.pdf"可能包含了一个具体的无锁循环队列实现的详细步骤和分析,"download链接.txt"可能是提供相关资料的下载链接,而"lock-free-wait-free-...

    一个无锁算法的库实现

    总的来说,`lockfree-lib`库为C程序员提供了一种实现高效并发的工具,但使用时需要充分理解无锁算法的原理和潜在问题,以确保在实际应用中达到预期的效果。在具体使用这个库之前,建议仔细阅读文档,理解其提供的...

    超低延迟的无锁队列atomic-queue

    无锁队列是一种高效的数据结构,它在并发环境中提供了高性能的线程间通信。在“atomic_queue”这个开源库中,我们看到一个基于原子操作的多生产者多消费者(Multiple Producer Multiple Consumer,简称MPMC)无锁...

    无锁队列Disruptor超详细教程

    无锁队列Disruptor是LMAX公司为解决内存队列延迟问题而设计的一种高性能队列,它在Java领域被誉为最高性能的队列。与Kafka等服务间消息队列不同,Disruptor主要用于线程间的消息传递。由于其卓越的性能,基于...

    高效环形队列 C

    环形队列是一种高效的数据结构,它在计算机科学中被广泛应用,特别是在并发编程和消息传递系统中。在C语言中实现环形队列,可以利用数组的线性特性,通过循环索引的方式达到高效的数据存取。这个压缩包中包含的`test...

Global site tag (gtag.js) - Google Analytics