lockFreeQueue 无锁队列实现与总结

无锁队列

介绍

  在工程上,为了解决两个处理器交互速度不一致的问题,我们使用队列作为缓存,生产者将数据放入队列,消费者从队列中取出数据。这个时候就会出现四种情况,单生产者单消费者,多生产者单消费者,单生成者多消费者,多生产者多消费者。我们知道,多线程往往会带来数据不一致的情况,一般需要靠加锁解决问题。但是,加锁往往带来阻塞,阻塞会带来线程切换开销,在数据量大的情况下锁带来的开销是很大的,因此无锁队列实现势在必行。下面就详细讲一下每种情况的不同实现方法。

单生产者单消费者

  从最简单的单生产者单消费者说起,假设我们现在有一个正常的队列,写线程往这个队列的head处push数据,读线程往这个队列的tail处pop数据。试想,如果head和tail不相同,也就是两个线程操作的数据不是同一个,这个时候是不会产生冲突的,这意味着我们只需要在head == tail的时候做处理。这时候可以发现,head == tail的时候,正是队列空的时候,也就是说这个时候读线程是读不到数据的,因此,读线程和写线程是不会产生冲突的,所以实现参考如下伪代码

bool push(T &data) {
    if (isFull()) { // head == tail + 1
        return false;
    }
    lockFreeQueue[tail] = data;
    // 注意编译器或者CPU可能会为了提升性能将代码乱序,为了上下两句代码不颠倒顺序,这里需要加上内存屏障
    tail++;
    return true;
}

bool pop(T &data) {
    if (ifEmpty()) { // head == tail
        return false;
    }
    data = lockFreeQueue[head];
    // 内存屏障
    head++;
    return true;
}

多生产者单消费者/单生成者多消费者

  参考单生产者单消费者模型我们可以发现,读和写本身是无竞争的,竞争的是读和读之间,写和写之间。考虑单生产者多消费者问题,有没有方式避免读和读之间的竞争呢?

MUTEX

  解决竞争最粗暴的方法,直接上锁。

原子操作CAS

  可以利用CAS模拟加锁解锁,定义一个变量当锁,然后将该变量置为0或者1代表未上锁和已上锁状态,因为操作是原子的,所以保证每次只有一个线程可以抢占到锁。c提供了一个__sync_bool_compare_and_swap接口使用,c++也有相关原子变量库。这种加锁解锁非常快,适合频繁加锁解锁的场景

队列分离

  如果真的不想加锁,最简单的方法就是,我根据读线程数分配队列数,也就说一个读线程对应一个队列,这样,读和读就可以分离开来,也就不存在竞争了。然后写依此去读每个队列,这样可以达到完全无锁。但是这会带来其他问题,如果某个线程处理比较慢,他绑定的队列里的信息无法及时清空,其他线程也无法帮他清空,可能会带来某些信息无法快速处理,导致超时。所以要根据实际情况选用这种模式。

队列共享

  可以发现,将队列分成多个的做法不是完全不可取的。在只有一个队列的情况下,所有读线程都会去竞争队头,在同一时间只会有一个线程可以抢到锁,然后进行读,再释放锁。如果我现在有多个队列,然后每个线程都可以读任意一个队列,当然在读之前还是要先抢占队列使用权,但是因为队列数量很多,可以把冲突尽可能分散掉。如果此时队列数量足够多,冲突率会更低。

  但是这又带来一个问题,如何给线程分配队列?最简单的方法是轮询,也就是每个线程一开始都在0号队列处,依次尝试去获取每个队列的使用权,抢不到的话就去获取下一个。这种做法简单粗暴,也可以很明显地看出冲突率十分高,因为每次都是取下一个队列,一旦一个队列抢占成功,后面的队列都要尝试去获取这个队列的使用权,如果前面的队列没有及时释放掉队列使用权,其他队列肯定会获取失败。

  我们想要每个线程都能获取到任意一个队列的使用权,又不想冲突率太高,可以用下面的方法。假设我现在有n个线程,k个队列,假设n<k,且n与k互质,一开始第i个线程在第i个队列处,处理完后移动到第(i + n) % k个队列,以此类推。因为n和k互质,可以保证每个线程都能访问到任意一个队列,而且线程之间不容易发生竞争。如果k足够大,基本上不会产生竞争。如果发生了竞争,可以采用自调节方式,下一个队列移动到第(i + n + 1) % k个队列处,这样的做的原因是,可以认为如果发生了竞争,那么后面也很容易竞争,所以改变轨迹是很有必要的。但是这种解决冲突的方法有一定局限性,首先如果队列数太多,那么一个线程累计下来的数据,另一个线程要去处理到它们的时间就会延长,也可能带来超时。所以其实冲突和及时处理本身就是矛盾的,二者无法完全避免。

共享带来的问题

  如果你的读写十分占cpu,可能需要每个线程分配一个核的时候,共享带来的问题是不可忽视的。每个CPU都有各自的Cache,具体跟CPU架构有关,但至少L1是每个核一个的。为了保持Cache同步,CPU采用了MESI协议去保证。简单来说就是每个核监听总线,可以知道哪些数据被修改过了,对于脏数据及时同步,同步要经过数据总线。如果每个线程绑定一个核,队列又是共享的,那CPU就要频繁进行同步。同步是必不可少的,但是如果你只有一个写线程,又有多个读线程的话,写线程会被大大影响,从而降低了性能。这是因为读线程所在的核需要和写线程所在的核进行同步,总线容易被占满,写自然就慢下来了。这种情况很难去优化,只能建议在绑核的时候,写线程尽可能和读绑在同个CPU上,跨CPU带来的消耗更大。或者可以利用超线程技术,超线程上的两个核是共享Cache的,可以把线程两两绑定起来,但是这样CPU算力会降低,因为超线程无法达到两个核的算力。这种情况只能从降低同步量去解决了。

  降低同步量的方法很多,尽可能使用线程私有的变量,而不是全局变量,这样不会带来同步。或者是一个线程连续处理多次同个队列,降低移动的频率,这样同步数量也可以减少,但是会带来上面讲过的数据处理延迟。

  False sharing 问题也是不可忽视的,cache同步的时候,两个连续的内存分别在两个核上,且在同个cache line。cpu每次更新数据的最小单位都是cache line。假设这两个数据都要频繁更新,那他们会不断向对方发生更新数据请求,这样开销十分巨大(具体参考MESI协议,这里不细说)。解决方法就是进行Cache line对齐,每个变量都分配到不同cache line上就好。

总结

  在做这次lockFreeQueue的过程中,我对CPU有了更多认识,很多简单的东西往往不是在算法上做优化,而是要在了解了CPU是怎么处理,OS是怎么处理,编译器是怎么处理之后,去做一些常数的优化。其实我做的优化远比上面讲的要多,诸如分支预测,数据预拉取的操作,但是和lockFreeQueue本身没有太大关系。本博文做记录用,有问题或者有更好解决方法的可以留言。

原文地址:https://www.cnblogs.com/scaugsh/p/10074297.html

时间: 2024-10-12 10:03:46

lockFreeQueue 无锁队列实现与总结的相关文章

环形无锁队列

环形无锁队列 Table of Contents 1 环形无锁队列的实现 2 死锁及饥饿 3 一些优化 1 环形无锁队列的实现 数据结构定义: template class LockFreeQueue { private: ElementT *mArray; int mCapacity; int mFront; int mTail; } 由于出队操作是在队首进行,入队操作是在队尾进行,因此,我们可以尝试用mFront和mTail来实现多个线程之间的协调.这其中会用到CAS操作: 入队操作伪码:

无锁队列的环形数组实现

对无锁队列的最初兴趣来自梁斌同志的一个英雄帖:http://coderpk.com/. 第一次看到这个题目的时候还不知道CAS,FAA等所谓的“原子操作”,但直觉上感觉,通过对读写操作的性能优化来达到大幅提高队列性能的方法是行不通的,就算读写操作全用汇编来写,也不会和正常的read及 write有数量级上的区别.后来搜索了一下lock free data structure,才知道了关于原子操作的一些东西,同时也纠正了自己由来已久的一个错误观点:C++中的++操作和--操作都不是原子操作.这篇笔

基于循环数组的无锁队列

在之前的两篇博客(线程安全的无锁RingBuffer的实现,多个写线程一个读线程的无锁队列实现)中,分别写了在只有一个读线程.一个写线程的情况下,以及只有一个写线程.两个读线程的情况下,不采用加锁技术,甚至原子运算的循环队列的实现.但是,在其他的情况下,我们也需要尽可能高效的线程安全的队列的实现.本文实现了一种基于循环数组和原子运算的无锁队列.采用原子运算(compare and swap)而不是加锁同步,可以很大的提高运行效率.之所以用循环数组,是因为这样在使用过程中不需要反复开辟内存空间,可

多个写线程一个读线程的无锁队列实现

在之前的一篇博客中,写了一个在特殊情况下,也就是只有一个读线程和一个写线程的情况下,的无锁队列的实现.其中甚至都没有利用特殊的原子加减操作,只是普通的运算.这样做的原因是,即使是特殊的原子加减操作,也比普通的加减运算复杂度高很多.因此文中的实现方法可以达到很高的运行效率. 但是,有的情况下并不是只有一个读线程和一个写线程.越是一般化的实现,支持的情况越多,但是往往损失的性能也越多.作者看到过一个实现(http://www.oschina.net/code/snippet_732357_13465

并发无锁队列学习之二【单生产者单消费者】

1.前言 最近工作比较忙,加班较多,每天晚上回到家10点多了.我不知道自己还能坚持多久,既然选择了就要做到最好.写博客的少了.总觉得少了点什么,需要继续学习.今天继续上个开篇写,介绍单生产者单消费者模型的队列.根据写入队列的内容是定长还是变长,分为单生产者单消费者定长队列和单生产者单消费者变长队列两种.单生产者单消费者模型的队列操作过程是不需要进行加锁的.生产者通过写索引控制入队操作,消费者通过读索引控制出队列操作.二者相互之间对索引是独享,不存在竞争关系.如下图所示: 2.单生产者单消费者定长

DIOCP开源项目-高效稳定的服务端解决方案(DIOCP + 无锁队列 + ZeroMQ + QWorkers) 出炉了

[概述] 自从上次发布了[DIOCP开源项目-利用队列+0MQ+多进程逻辑处理,搭建稳定,高效,分布式的服务端]文章后,得到了很多朋友的支持和肯定.这加大了我的开发动力,经过几个晚上的熬夜,终于在昨天晚上,DEMO基本成型,今天再加入了QWorkers来做逻辑处理进程,进一步使得逻辑处理进程更加方便和高效.今天特意写篇blog来记录我的心得与大家分享. [功能实现说明] 沿用上次的草图 目前DEMO图上的功能都已经实现.下面谈谈各部分的实现. 通信服务, 由DIOCP实现,担当与客户端的通信工作

无锁队列的实现

锁是高性能程序的杀手,但是为了保证数据的一致性,在多线程的应用环境下又不得不加锁.但是在某些特殊的场景下, 是可以通过优化数据结构来达到无锁的目的.那么我们就来看一下如何实现一个无锁队列. 队列:众所周知,就是先进先出. 出队列的时候从队列头取出一个结点:入队列的时候,将结点添加到队列尾部.当多线程同时操作一个队列读写时,显然就需要加锁.但是在单读单写的这种多线程应用时,是可以做到无锁的.直接上代码 #ifndef _QUEUE_H_ #define _QUEUE_H_ template<cla

C++ boost库无锁队列多线程并行测试与编译方法

阅读了网络中关于Boost库无锁队列的源代码,但却缺少编译方法.经过测试,确定了ubuntu 14.04中编译boost库的方法,特做记录. 无锁(free-lock)是实现高性能多线程并发编程的重要技术. 作为C++11 STL参考实现的boost库,不仅支持11标准,而且做了许多扩展,掌握其使用方法,对于提高代码质量,尤其重要. 以其多线程并行无锁队列为例,结合代码和说明,演示了无锁boost库的使用和编译方法. 代码及说明如下: //source: boost_queue.cpp //目的

多线程编程之无锁队列

关于无锁队列的概念与实现,可以参考博文<无锁队列的实现>,主要涉及到的知识点包括CAS原子操作.无锁队列的链表实现.无锁队列的数组实现以及ABA问题. 下面借鉴了<多线程的那点儿事(之无锁队列)>的代码,说明两个线程(一个添加一个读取数据)之间的无锁队列,可以不借助线程互斥方法就能够达到并行效果.代码如下: #define MAX_NUMBER 1000L #define STATUS int #define OK 0 #define FALSE -1 typedef struct