给产品经理讲技术丨技术进阶:生产者与消费者模式

我是创始人李岩:很抱歉!给自己产品做个广告,点击进来看看。  

【文章摘要】今天闲话少说,咱们先来看一个例子。

1

 

【相关推荐】

给产品经理讲技术丨乱码导致的悲剧告白

给产品经理讲技术丨把URL五马分尸

给产品经理讲技术丨没线,并不可怕?

给产品经理讲技术丨提需求的正确姿势是什么

给产品经理讲技术丨产品后悔药来了,讲讲热补丁技术

为了响应国家万众创业的号召,你开了一家外卖餐厅。餐厅里有一台电话,可以接受订单。有一台电瓶车,你平时就骑着去送餐(我们先假设菜都是事先做好的)。

一开始的时候,整个过程是单线程的,你首先接电话接受订单,然后骑电瓶车去送餐。送完餐再回来接电话,以此循环。我们假设接电话需要5分钟,送餐半小时,那么每个订单需要35分钟处理完毕,如果有两个用户同时打来电话的话,其中一个就要等待35分钟才能被受理。

可见单线程的模式体验非常差,机智的你自知此路不通,开始谋划多线程并发的方案。咱们借此机会先来复习一下多线程的知识。

多线程是一种并发执行的技术,主要目的是最大化的利用各种资源,提高程序整体性能。在我们计算机中,CPU、磁盘、显卡、网络带宽这些都是资源。比如拷贝文件的时候,主要在使用磁盘资源,这时候其他资源是闲置的,如果使用多线程,就可以把CPU、网络带宽资源利用起来,做到一边拷贝文件,一边打LOL。在上面的例子里,订餐的电话、送餐的电瓶车都是资源。之所以单线程的方式体验差,就是因为在使用电瓶车送餐的时候,电话资源一直是空闲的,这就是一种浪费。这里需要注意一点,在有些场景里,资源是单一的(比如单核CPU计算圆周率),这时候使用多线程是没有意义的,反而会因为线程之间的切换造成额外的开销。

作为一种改进,你雇了一个小伙子帮你送餐,你就专心在店里接电话。现在终于可以多线程并行工作了:一个线程用来处理送餐任务,我们暂且命名为送餐线程,一个线程用来处理下订单任务,叫订餐线程。二者可以同时工作。

但是问题也随之而来。很显然,送餐线程和订餐线程是相互依赖的,换句话说,没有订餐,哪里来的送餐任务呢?小伙子出去送餐了,这期间即使下了订单也送不了,那电话到底是接还是不接呢?这样的结果就是,两个线程互相依赖,互相等待,整体效率和前面单线程差不多。这就是所谓的并发协作问题,虽然系统支持并发,但是因为协作不好,效率一样低下。

如何解决这样的问题呢?很简单,只需要添加一个订单箱就行了。订餐线程不停的接受订单,然后把订单丢到订单箱里。送餐线程先检查下箱子里有没有订单,有的话就取出一个去送餐,以此循环。当然,因为订餐线程只需要5分钟就可以生成一个订单,而送餐线程需要半小时才能消化一个订单,必然会造成订单箱里的订单越来越多。这时你可以设置一个上限,比如最多20个,超过20订餐线程就不工作了,再有客户打来电话你就不要接了。有的时候生意不好,送餐回来发现没有订单,这时候大家就等着,直到有新的订单产生。这样一来两个线程各自为政,互不干扰,问题就解决了。

这就是生产者消费者模式的基本思想。在上面的例子里,订餐线程就是生产者,生产的是「订单」。送餐线程是消费者,消费的也是「订单」。这个模式的精髓在于,它定义了一个「缓冲区」,就是上面的订单箱,把生产者和消费者隔离开来,让他们不用彼此依赖,各自开开心心的干活。这个其实是很难得的,软件开发讲究「解耦」,最好的情况是大家虽然在一起干活,但是不用担心彼此的实现细节。对于生产者而言,它只能看到「缓冲区」,对于消费者,它也只能看到「缓冲区」,就是这个意思。「解耦」的代码很优雅,不光程序员看着舒服,还可以用来应对天天改需求的。今天开餐厅,「缓冲区」里是「订单」,明天改成开电影院,「缓冲区」里就是「电影票」,换汤不换药,他好我也好。

Talk is cheap,看个实际例子。现在很多APP,像微博知乎今日头条这样的,都有Feeds流。

2.webp

当用户快速滑动Feeds列表的时候,会有很多图片加载出来。每一张图片的加载都会经历「网络下载」、「图片解码」、「图片显示」几个阶段。其中「网络下载」是把图片的原始数据从服务器上拉取下来,是一个非常消耗网络带宽资源的过程,其速度受限于你的网络速度(WIFI、4G要快些,2G、3G慢)。「图片解码」是把从网上拉取到的原始数据解码成可以显示的像素数据,例如从网上拉取到一张jpeg格式压缩过的图片,这一步会把这张图片解码成通用的图片格式,抹去jpeg编码的信息。这一步主要是由CPU完成的。最后图片显示,我们简单理解成一个GPU密集型任务。

现在,如果用生产者消费者模式来设计Feeds流的图片加载过程的话,我们需要两个「缓冲区」。一个是「原始数据缓冲区」,里面盛放着从网络下载下来的原始数据。对它而言,生产者是「网络下载」线程,消费者是「图片解码」线程。另一个是「图片缓冲区」,里面盛放着解好码的图片,对它而言,生产者是「图片解码」线程,消费者是「图片显示」线程。整个过程一环扣一环,但是又相互独立:对于「网络下载」线程来说,它只管拼命下载,下载完一张就丢到「原始数据缓冲区」,然后去下载另一张;对「图片解码」线程来说,它只管消费「原始数据缓冲区」里的原始数据,然后生产解码好的图片放入「图片缓冲区」。「图片显示」线程则只管消费「图片缓冲区」中图片。整个过程最大化的利用了CPU、GPU和网络带宽资源。

有一个细节需要注意。如果你的网速很慢,「网络下载」线程运行缓慢,但是「图片解码」线程速度很快,不一会儿「原始数据缓冲区」就空了。这时候「图片解码」线程就会一直停在那里等待,这个等待会不会造成CPU占用率100%呢?不会的。你要把CPU的「等待」和「死循环」区别开来。虽然「死循环」也可以实现这个功能,就是一直不停的询问缓冲区有没有新的数据,但是效率很低,又不环保。现代操作系统都支持「等待」、「唤醒」,本质上是利用CPU的中断机制,可以做到只要缓冲区里又有了数据,马上通知消费者。这也是之前讲过的轮询和回调的一种简单应用。

生产者消费者模式在计算机领域还有很多应用的实例,很多多线程并发协作的问题都可以用它解决,是程序员的必修课之一。

今天心情不大好,别的就不多说了,明天见。

欢迎添加微信公众号:给讲技术

欢迎添加微信公众号:给产品经理讲技术

 

随意打赏

提交建议
微信扫一扫,分享给好友吧。