环形缓冲区是lock-free的原因:lock-free即绝对无锁,环形缓冲区相当于一个队列,在这个队列中,只有一个生产者和一个消费者,不管是生产者还是消费者,都不需要完全独自霸占整个队列,所以不存在满的情况,不需要对整个数据结构进行加锁。
一、环形缓冲区是lock-free的原因
lock-free就是绝对无锁。这其中有三个前提,一是cpu支持内存栅栏,二是数据的地址必须是四对齐的,三是必须是一个生产者对应一个消费者。这三个必要前提缺一不可,否则就没有办法实现lock-free了。
所谓的环形缓冲区实际上就相当于一个队列,在这个队列中,只有一个生产者和一个消费者,但是不管是生产者还是消费者,都不需要完全独自霸占整个队列,他们都只是移动首尾,也就是数据的输入和删除,变化的只是环形缓冲区中空间的位置分配。所以不存在满的情况,也不需要对整个数据结构进行加锁。
二、Lock-Free编程
1、Lock-Free编程是什么
当谈及 Lock-Free 编程时,我们常将其概念与 Mutex 或 Lock 联系在一起,描述要在编程中尽量少使用这些锁结构,降低线程间互相阻塞的机会,以提高应用程序的性能。类同的概念还有 “Lockless” 和 “Non-Blocking” 等。实际上,这样的描述只涵盖了 Lock-Free 编程的一部分内容。本质上说,Lock-Free 编程仅描述了代码所表述的性质,而没有限定或要求代码该如何编写。
基本上,如果程序中的某一部分符合下面的条件判定描述,则我们称这部分程序是符合 Lock-Free 的。反过来说,如果某一部分程序不符合下面的条件描述,则称这部分程序是不符合 Lock-Free 的。
从这个意义上来说,Lock-Free 中的 “Lock” 并没有直接涉及 Mutex 或 Lock 等互斥量结构,而是描述了应用程序因某种原因被锁定的可能性,例如可能因为死锁(DeadLock)、活锁(LiveLock)或线程调度(Thread Scheduling)导致优先级被抢占等。
Lock-Free 编程的一个重要效果就是,在一系列访问 Lock-Free 操作的线程中,如果某一个线程被挂起,那么其绝对不会阻止其他线程继续运行(Non-Blocking)。
下面的这段简单的程序片段中,没有使用任何互斥量结构,但却不符合 Lock-Free 的性质要求。如果用两个线程同时执行这段代码,在线程以某种特定的调度方式运行时,非常有可能两个线程同时陷入死循环,也就是互相阻塞了对方。
while (x == 0)
{
x = 1 - x;
}
所以说,Lock-Free 编程所带来的挑战不仅来自于其任务本身的复杂性,还要始终着眼于对事物本质的洞察。
通常,应该没有人会期待一个大型的应用程序中全部采用 Lock-Free 技术,而都是在有特定需求的类的设计上采用 Lock-Free 技术。例如,如果需要一个 Stack 类应对多线程并发访问的场景,可以使用 Lock-Free 相关技术实现 ConcurrentStack 类,在其 Push 和 Pop 操作中进行具体的实现。所以,在使用 Lock-Free 技术前,需要预先考虑一些软件工程方面的成本:
- Lock-Free 技术很容易被错误的使用,代码后期的维护中也不容易意识到,所以非常容易引入 Bug,而且这样的 Bug 还非常难定位。
- Lock-Free 技术的细节上依赖于内存系统模型、编译器优化、CPU架构等,而这在使用 Lock 机制时是不相关的,所以也增加了理解和维护的难度。
2、Lock-Free编程技术
当我们准备要满足 Lock-Free 编程中的非阻塞条件时,有一系列的技术和方法可供使用,如原子操作(Atomic Operations)、内存栅栏(Memory Barrier)、避免 ABA 问题(Avoiding ABA Problem)等。那么我们该如何抉择在何时使用哪种技术呢?可以根据下图中的引导来判断。
三、环形缓冲区
1、环形缓冲区是什么
初学者一般使用的buffer是线性的,数据依次排列依次读取,就像流水线。造成的问题就是,处理大量数据时,需要大段内存,并且需要考虑对内存的管理。频繁的内存分配不但增加系统的开销,更使得内存碎片不断增多,非常不利于程序长期运行。
环形缓冲区使用一段固定长度的内存,在内存用尽后,剩余未存的数据从这段内存的起始位置开始存放。就像两个人围着一张原型的桌子追逐,一个写,一个读。这样反复使用内存,能使得我们能使用更少的内存块做更多的事情,并且对内存的管理更加方便更加安全。
2、如何使用环形缓冲区
- 使用环形缓冲区时,定义了两个指针,一个写指针,一个读指针。读指针指向环形缓冲区可读数据的名列前茅个数据地址,写指针指向环形环形缓冲区可写数据的名列前茅个数据地址。
- 在进行写操作时,需要先进行判断环形缓冲区是否已写满,若已写满,最直接简单的方式就是直接覆盖原先已写的数据;其次依据实际的应用做相应的处理。
- 在进行读操作时,需要进行判断环形缓冲区是否为空,若为空则无法读取数据。
- 环形缓冲区的核心精华在于对读写指针移动进行取模求余运算,计算出当前的位置,用于判断环形缓冲区当前的状态(空、满)。
- 当Read_Index = Write_Index时,说明环形缓冲区为空。
- 当((Write_Index + 1)% RingBuffer_Size) = Read_Index时,说明环形缓冲区已满。
- 若有多个任务需要读写环形缓冲区时,必须添加互斥保护机制,确保每个任务均正确访问环形缓冲区。
延伸阅读1:缓冲区是什么
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。