最佳实践生产者和消费者模式中的双缓冲技术

Posted IOT物联网小镇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最佳实践生产者和消费者模式中的双缓冲技术相关的知识,希望对你有一定的参考价值。

【这篇文章说了啥】

    这篇文章主要介绍了在生产者-消费者模式中,生产和消费之间有大量数据需要交互时的一个高效率的解决方案。

 

【问题引入】

1. 问题场景

    在设计模式中,生产者-消费者模式肯定是排在前面位置的,在实际开发过程中,也常常需要使用这个模式。

 

    在讲解设计模式的书籍中,只会从抽象的角度对生产者-消费者模式进行讲解。面对实际的编程问题,还需要具体问题具体分析

    这里给出一个使用场景,你可以停下来思考一下该如何解决问题:

    你需要实现一个日志库,每一个线程调用日志库API函数来写入日志信息,所有的日志信息需要持久化到本地的文件系统。

 

    咱们来分析一下,有哪些问题需要解决:

(1)多线程调用日志库的API,所以API函数必须是线程安全的。

(2)日志信息无法预计频率,要考虑波峰和波谷。

(3)日志信息需要持久化到本地文件系统中,涉及到文件操作,而文件的IO操作速度相对于CPU来说是很慢的。

(4)每一条日志信息不应该是立刻写入到文件,而是应该缓存在内存中,当数据量积累到一定的大小再写入到文件(当然,也要考虑定时写入文件)。

 

2. 解决思路

    咱们再回到生产者-消费者模式上。书籍上在介绍这种模式时,一般都是同步模式,即:

        生产者产生一个数据后通知消费者,然后等待数据被“消费”;

        消费者收到生产者的通知后,“消费”数据,然后再通知生产者继续生产。

    生产和消费交替执行,所以我称之为同步模式。

    但是,在上面所说的日志系统中,显然不能用同步模式。因为日志的产生速度与写文件速度是无法预估的,例如:在某个场景下日志的产生速度非常快,而在另一个场景下日志的产生速度特别慢。

 

    对于这样的需求,生产者(日志的产生)和消费者(把日志写入文件)速度不匹配,显然应该使用不同的线程来执行。此时,你是不是立刻想到使用消息队列来进行数据缓冲,不就解决了这个速度不匹配的问题?也就是这样:

    生产者:往队列的头部插入日志信息(入队)

    消费者:从队列的尾部读取日志信息(出队)

 

    当然,你还考虑到因为他们是不同的线程,在操作同一个队列时,需要用一个锁(Mutex)来保护消息队列。大概就是这个样子:

 

    看起来很完美,但是有一个问题:消费者从消息队列每读取一条日志信息就,写入文件系统,但是写文件操作是很耗时的。频繁的从消息队列中获取数据,而且每次都要上锁,一定会对生产者的写日志效率产生影响,因为生产者也要对消息队列上锁才能把日志信息插入队列的头部,如果此时消息队列正好被消费者锁住了,那么生产者就必须伤心的等待了~~这样就会很大影响到日志系统整体的吞吐率。

 

3. 使用双缓冲

    既然消费者的写文件速度比较慢,一定不能影响了生产者的写入效率,所以我们可以用两个消息队列来分别存储:正在写入的日志信息,正在读取的日志信息,也就是所谓的双缓冲”技术。用图画出来就是这个样子:

 

 

    注意上图中,内存中用来缓存日志的空间不一定要用消息队列,因为日志信息往往都是字符串类型,直接用一块连续的堆或栈的空间来存储就可以了,所以图中就用“缓冲区1”、“缓冲区2”来表示。

    

    在这个模型中,生产者向缓冲区1中写日志信息;而消费者从缓冲区2中读取日志信息,这样的话,消费者的写文件操作无论怎么慢都不会影响到生产者产生日志了。

    

    此时,你肯定要说:缓冲区1和缓冲区2是两个独立的内存空间,当缓冲区1被写满后如何把其中的内容“复制”到缓冲区2中呢?

    好问题,这也是日志系统实现高吞吐率的关键地方!

 

4. 缓冲区交换

    最直觉的想法就是在某个时刻(比如:缓冲区1写满了,缓冲区2空了,定时),把缓冲器1中的内容用memcpy或者其他的系统函数,复制到缓冲区2中。 当然在复制数据的过程中需要对这两个缓冲区都上锁,在临界区完成复制或者移动操作,而且这个移动操作要尽可能的快,这样才能对生产者和消费者产生最小的影响。但是如果数据量比较大,移动操作还是比较耗时

 

    再仔细想想,其实我们需要的不是真正的移动操作,而是有一个地方让生产者存放产生的数据,同样的有一个地方让消费者读取数据,只要达成这个目的就可以了。

 

    还有一个更好的方法,就是直接交换两个缓冲区的地址。我们只需要把生产者在写入数据时的指向缓冲区1的指针重新指向缓冲区2, 把消费者读取数据时指向的缓冲区2的指针重新指向缓冲区1,这样就达到了交换缓冲区的目的了。

    交换缓冲区之前:生产者向缓冲区1中写日志,消费者从缓冲区2中读日志。

    交换缓冲区之后:生产者向缓冲区2中写日志,消费者从缓冲区1中读日志。

 

    在执行交换操作的时候,也需要对这两个缓冲区上锁。但是在这个临界区操作的是:交换两个指针所指向的缓冲区空间,所以执行速度会非常快。

    具体到语言层面,对于C来说就是交换两个4字节的地址,对于C++来说可以利用容器类型的swap函数。

    这样画图更好理解:

 

    图中:左侧是执行交换操作之前的样子,右侧是执行交换操作之后的样子。可以看到生产者和消费者在任意时刻操作的都是不同的缓冲区,所以不存在相互影响,而且也达到了快速交换内容的目的。

    通过这样的双缓冲技术实现的日志系统,实际测试下来发现,吞吐率比很多开源的日志库要高很多。大家如果有兴趣,可以简单测试一下。

 

【总结】

    写到这里,我想表达的内容基本结束了。

    你可能还有其他的一些疑点,比如:什么时候交换缓冲区?写入缓冲区满了之后怎么处理?这些就属于另一个话题了。

    在这个实际的使用场景中,通过双缓冲技术,很好地解决了生产者和消费者之间的异步操作和速度不匹配问题,提高了日志系统的整体吞吐率。

 

【打完收工】

不知道这篇文章是否给你带来小小的帮助?

另外,评论和转发都是免费的哦~~~

我会持续分享嵌入式开发过程中的各种最佳实践,欢迎关注微信公众号:IOT物联网小镇

以上是关于最佳实践生产者和消费者模式中的双缓冲技术的主要内容,如果未能解决你的问题,请参考以下文章

双缓冲队列-减少生产者消费者锁的调用

生产消费模式

并行模式之生产者-消费者模式

Qt 中的最佳生产者/消费者线程模式

生产者消费者简单实现(转载)

生产者消费者模式