背景

由于最近在做一个项目,但是框架本身有个不合理的设计。其中的代码是单线程的,数据的读取和计算都在一个线程里面完成。也就是说,我们的程序有很大的一部分时间在读取文件数据,导致最终的运行速度很慢。这里就可以使用多线程来优化。

这里需要使用最基本的生产者消费者模式。

使用若干个线程作为生产者,负责数据的读取和预处理,这部分任务是IO密集型的,也就是不太占CPU,但是比较占带宽,而且有延时。在处理完数据之后,将数据放到一个队列中。

同时,使用若干个线程充当消费者,从这个队列里面获取数据,然后进行计算。计算的部分是CPU密集型的(其实我这里计算是GPU做的,就只有一个消费者),计算完成之后输出结果。

那么贯穿这一整套方案的,就是我们的队列。

在并发任务中,通常都需要一个队列机制,将并行的任务转化成串行的任务,或者将串行的任务提供给并行工作的线程。这个队列会同时被多个线程读写,因此也必须是线程安全的。

线程安全的实现策略

对于线程安全的队列的实现,似乎经常成为企业的面试题,常见的实现方法就是互斥量和条件变量,本质上就是锁的机制。同一时间只有一个线程具有读写的权限。锁的机制在并发量不大情况下,十分的清晰有效。在并发量较大的时候,会因为对锁的竞争而越发不高效。同时,锁本身也需要维护一定的资源,也需要消耗性能。

这时候,大家肯定会想问,不使用锁机制,还可以处理这种并发的情况吗?

答案是肯定的,首先我们知道锁主要有两种,悲观锁乐观锁。对于悲观锁,它永远会假定最糟糕的情况,就像我们上面说到的互斥机制,每次我们都假定会有其他的线程和我们竞争资源,因此必须要先拿到锁,之后才放心的进行我们的操作,这就使得争夺锁成为了我们每次操作的第一步。乐观锁则不同,乐观锁假定在很多情况下,资源都不需要竞争,因此可以直接进行读写,但是如果碰巧出现了多线程同时操控数据的情况,那么就多试几次,直到成功(也可以设置重试的次数)。

我们生活的时候,总会碰到很多的不顺心的事情,比如模型训练崩了,被某些库搞得头大,或者女票又生气了什么的,不妨学习一下乐观锁的精神,再训一次?再编译一次?大不了再哄一次。一次不行就两次。

回到乐观锁上,乐观锁中,每次读写都不考虑锁的存在,那么他是如何知道自己这次操作和其他线程是冲突的呢?这就是Lock-free队列的关键——原子操作。原子操作可以保证一次操作在执行的过程中不会被其他线程打断,因此在多线程程序中也不需要同步操作。在C++的STL中其实也提供了atomic这个库,可以保证多线程在操控同一个变量的时候,即使不加锁也能保证起最终结果的正确性。而我们乐观锁需要的一个原子操作就是CAS(Compare And Swap),绝大多数的CPU都支持这个操作。

CAS操作的定义如下(STL中的一个):

bool atomic_compare_exchange_weak (atomic<T>* obj, T* expected, T val);

首先函数会将obj与expected的内容作比较:
如果相等,那么将交换obj和val的值,并返回true。
如果不相等,则什么也不做,之后返回false。

那么使用这个奇怪的操作,为什么就可以实现乐观锁了呢?这里我们看一个例子。这也是我学习的时候看的例子。

struct list {std::atomic<node*> head;
};...void append(list* s, node* n)
{node* head;do {head = s->head;n->next = head;} while (!std::atomic_compare_exchange_weak(&(s->head), &head, n));// or while (!s->head.compare_exchange_weak(head, n));
}

在我们向list中插入元素的时候,首先获取到当前的头指针的值head,然后我们在写数据的时候,首先和此刻的头指针值作对比,如果相同,那么就把新的节点插入。如果不相同,说明有线程先我们一步成功了,那么我们就多尝试一次,直到写入成功。

以上就是使用CAS操作实现的乐观锁。上面的这个append就是最简单的Lock-free且线程安全的操作。

concurrentqueue

最近在做这个项目的时候,就被安利了一个header only的C++并发队列库concurrentqueue。本着不重复造轮子的原则,我在项目中用了这个库,由于它只是两个头文件,特别方便的就加入到了项目中。关于这个库的特点,项目的github上写了很多。这里直接照搬下来,不做解释。

我体验了一下,感觉最舒服的有以下几点:

  1. 这个库确实可以很好的实现线程安全队列,而且速度很快。接口也比较简单。很容易上手。
  2. 整个库就是两个头文件,而且没有其他的依赖,使用C++11实现,兼容各大平台,很容易融入项目。
  3. 这个并发队列支持阻塞和非阻塞两种。(只在获取元素的时候可以阻塞)

因为这个队列的用法十分简单,这里就直接贴上官网的介绍,然后针对一些细节,补充说明一下。

非阻塞队列常用接口

ConcurrentQueue(size_t initialSizeEstimate) Constructor which optionally accepts an estimate of the number of elements the queue will hold
enqueue(T&& item) Enqueues one item, allocating extra space if necessary
try_enqueue(T&& item) Enqueues one item, but only if enough memory is already allocated
try_dequeue(T& item) Dequeues one item, returning true if an item was found or false if the queue appeared empty

ConcurrentQueue(size_t initialSizeEstimate)这个没什么好说的,一个构造函数,可以指定队列的容量。

enqueue(T&& item)入队操作。比较有意思的是,如果我们的队列已经满了的话,那么这个还是会把数据放到队列里,使得队列的容量变大。所以,如果希望队列的长度不变的话,尽量还是不要使用这个函数。

try_enqueue(T&& item)这个也是入队操作,与上一个不同,这个函数当队列已经满了的时候,并不会进行入队操作,而是返回一个bool类型的值,表示是否入队成功。我在使用的时候,会判断这个bool值,如果是false,就让线程等待10ms之后重试。

try_dequeue(T& item)这个是出队操作,如果队列有值的话,则得到数据(放到参数item里面)。他也会返回一个bool类型的值,表示时候出队成功。

阻塞队列常用接口

wait_dequeue(T&& item)wait_dequeue_timed(T&& item, std::int64_t timeout_usecs)

这两个函数的功能类似,都是进行出队操作,如果队列为空,则等待。唯一的区别是,前者永久等待,而后者可以指定等待的时间,如果超时,则会停止等待并返回false。

最后是块操作,两种模式的队列都支持批量插入的操作。这里,我没有用过这些接口,所以大家自行看文档就好。

无锁队列 concurrentqueue介绍(转载)相关推荐

  1. 基于数组的无锁队列(译)

    2019独角兽企业重金招聘Python工程师标准>>> 1 引言 最近对于注重性能的应用程序,我们有了一种能显著提高程序性能的选择:多线程.线程的概念实际上已经存在了很长时间.在过去 ...

  2. Erlang运行时中的无锁队列及其在异步线程中的应用

    本文首先介绍 Erlang 运行时中需要使用无锁队列的场合,然后介绍无锁队列的基本原理及会遇到的问题,接下来介绍 Erlang 运行时中如何通过"线程进度"机制解决无锁队列的问题, ...

  3. 你应该知道的高性能无锁队列Disruptor

    1.何为队列 听到队列相信大家对其并不陌生,在我们现实生活中队列随处可见,去超市结账,你会看见大家都会一排排的站得好好的,等待结账,为什么要站得一排排的,你想象一下大家都没有素质,一窝蜂的上去结账,不 ...

  4. java轻松实现无锁队列

    1.什么是无锁(Lock-Free)编程 当谈及 Lock-Free 编程时,我们常将其概念与 Mutex(互斥) 或 Lock(锁) 联系在一起,描述要在编程中尽量少使用这些锁结构,降低线程间互相阻 ...

  5. 无锁队列以及ABA问题

    队列是我们非常常用的数据结构,用来提供数据的写入和读取功能,而且通常在不同线程之间作为数据通信的桥梁.不过在将无锁队列的算法之前,需要先了解一下CAS(compare and swap)的原理.由于多 ...

  6. 原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析

    原理剖析(第 012 篇)Netty之无锁队列MpscUnboundedArrayQueue原理分析 - 一.大致介绍 1.了解过netty原理的童鞋,其实应该知道工作线程组的每个子线程都维护了一个任 ...

  7. 无锁队列的实现 | 酷壳 - CoolShell.cn

    无锁队列的实现 | 酷壳 - CoolShell.cn 无锁队列的实现 | 酷壳 - CoolShell.cn 无锁队列的实现 posted on 2013-01-14 11:30 lexus 阅读( ...

  8. 无锁队列与有锁队列性能比较

    最近研究boost的无锁队列,测试了一下性能,发现无锁队列比有锁的还要慢 testqueue.cpp #include <time.h> #include <boost/thread ...

  9. 无锁队列原理及实现(一)

    背景 在进行实际生产多线程开发的时候通常不会直接使用使用锁机制来操作线程间传递的数据,特别是对效率要求很高的场景中.最典型的就是音视频项目或者网络项目.这里先拿网络传输场景举例, 从这篇开始就开始详细 ...

最新文章

  1. 【机器学习基础】数学推导+纯Python实现机器学习算法21:马尔可夫链蒙特卡洛...
  2. java set第n位_Java学习路线:float在内存中的存储
  3. VirtualBox 安装失败的主要原因 不是正版的OS,系统主题需要还原
  4. [NewLife.XCode]高级增删改
  5. selenium+python自动化80-文件下载(不弹询问框)
  6. rcnn spp_net hcp
  7. JavaScript 音频插件和图表插件
  8. 家庭用计算机怎样选择设置网络位置,win7系统怎么选择网络位置
  9. 3G移动通信技术分析
  10. EL表达式(Exprission language)
  11. sql简介_SQL简介
  12. Unity3D调用外部程序
  13. excel熵值法计算权重_熵权法评价估计详细原理讲解
  14. 网络安全系列-X: TCP/IP协议及报文格式详解
  15. 利用POI将word转换成html实现在线阅读
  16. 大神TP_萌新到大神的必修课---分路篇v
  17. IBM主机系列课程之单元测试(基础篇)-李海湜-专题视频课程
  18. 华芯微特MCU——SWM181
  19. linux自定义命令-通过关键字批量杀死进程
  20. wireshark--抓包-zb-命-tcpdump

热门文章

  1. 自动化机床上下料流水线项目(鑫金雨)非标项目解决方案
  2. android分组流式布局,Android 流式布局实现
  3. IE浏览器使用js调用阅读插件调用二代证阅读器读取身份证信息
  4. 【JFinal】解决activerecord字段顺序问题
  5. 杜比dss200服务器重装,杜比dss200服务器恢复系统操作
  6. 服务器硬盘sas速度多少,服务器硬盘SAS接口和SATA接口哪个速度快,它们分别有什么优缺点?...
  7. valgrind 工具介绍和简单的使用
  8. [绍棠] 应用内支付(IAP)详解
  9. 网站推广的途径有以下几种:
  10. nginx代理离线瓦片,并在leaflet中调用