前言
目前我是一名Golang/Python开发工程师,之前是主要使用PHP进行开发的传统web后端工程师,后面因为工作原因开始接触并使用Python和Golang来做一些开发工作,涉及到数据分析数仓建设相关及部分游戏相关的开发;也因为工作原因接触到了很多其他语言的特性或者是其他语言团体推崇的技术方向方案。
我非常喜欢PHP,生活中工作中几乎是能用PHP解决的都尽可能使用PHP,同时也很推崇PHP-cli的开发模式,尤其喜欢workerman/webman
,早期webman还未诞生的时候我在公司曾使用workerman自建了一套框架,与现在的webman非常相似。
今天我要分享的主要是我所理解的协程相关的分享内容,内容会涉及到进程和线程相关的内容点,主要目的是为大家揭开协程神秘的一些面纱,让大家知道协程并没有那么的难,它其实是一种非常简单易懂的编程方式方案。
在阅读本分享前,建议先阅读之前的分享趣谈程序演变的过程,有助于理解本分享内容。
阻塞/非阻塞
在文章趣谈程序演变的过程中我曾提到两个概念内容,阻塞与非阻塞;如何理解阻塞与非阻塞呢?很简单:
阻塞
我去超市买一袋橘子:我需要穿好衣服、下楼、走路、到超市挑选橘子、付款、走路、上楼、到家;在这个流程中,我在买好橘子之前全程被占用,需要做的所有事都需要为买橘子服务,当我做完这一系列事情之后才可以干下一件事情,比如写代码,比如拉屎等等。
我们可以发现阻塞是一种非常简单且占用资源比较少的情况,因为全程只需要占用我一个人,只是需要花费的时间比较长。
非阻塞
我通过外卖来实现去超市买一袋橘子:我只需要通知外卖员告知他去xx超市帮我买橘子仅此而已,然后等待外卖员将橘子送达我住处即可;在这个流程中,我在买好橘子之前全程没有被占用,我随时都可以干任何事情,比如吃饭、写代码、拉屎,随意做我想要做的事情。 但是这里其实要注意,为了实现这种情况,我们必须要有一个外卖员来帮我们实现,同时我们在告知外卖员的这个过程中其实是被阻塞的,我们在和外卖员沟通的这个过程中。是没办法做其他事情的。
我们由此可以发现,非阻塞在某一个微观内其实多多少少包含了阻塞的部分存在的,并且整体是通过消耗更多的资源(人),从而减少时间的消耗。
进程/线程
上面讲了非阻塞,讲了资源,那么资源在系统里面如何实现如何使用呢?答案就是进程或者线程(关于进程和线程的概念我这里就不多赘述了,百度都有,可以自行百度)。
PHP中通常来说不使用多线程进行编程,通常来说都是使用多进程来实现一些并发效果的,比如workerman/webman就是用了fork来进行多进程的处理,通过不同的onWorkerStart的业务逻辑来实现不同的业务进程,每种业务进程都可以有自己的单/多进程处理方案。
协程
我们现在常谈的协程
,实际上严格意义上来说叫协程方案
,它包含了三样东西在其中:
- 协程
- 协程调度
- 协程执行
协程在一些语言实现中或者在一些文章中又叫纤程,PHP中的fiber、yield分别是有栈协程和无栈协程(关于这个概念可以自行百度,不影响本次分享的内容理解);协程本身不具备并发并行能力,它只是一种代码执行的方案,它仅仅需要实现中断、唤醒即可,这也是fiber、yield的基本功能。
这么看来,好像协程并没有什么用对吧,它好像只能暂停/继续,为什么我们非要实现这样的功能呢?
如果这么想,其实我们就陷入了阻塞模式的思维方式,我们盲目的把写的代码跟独自买橘子联系在了一起,从上往下读,在哪暂停就在那继续;看似没用,但假设我们用非阻塞模式去思考它,把我们整体的行为按照更小的颗粒度拆分成不同的小行为,交给不同的“外卖员”执行,那么这样,是不是就不一样了?
由于外卖员数量可能存在限制,不可能是无限多个,那么外卖员就需要根据具体情况具体分析,暂时放下手中的小行为,换另一个小行为先行,同时外卖员也可能找其他外卖员代工,在这个过程中,如果合理利用时间差,那么每个人都不需要花费太多的时间,合理利用了已存在这世界上的其他外卖员。
对,这就是协程调度器要做的事情,通过一些合理的规划来进行调度,而外卖员就是协程执行单元!
这里有许许多多的调度规则,根据不同语言或者不同方案有各自的实现方式,这里可以自行百度时间分片,有助于理解调度规则。
至于外卖员,是多线程/单线程实现还是多进程实现都可以。
延申问题
上述内容,其实我们可能会延申出来一些疑惑:
如果不同的小行为之间需要存在关联关系,外卖员又可能存在找其他外卖员代工的情况,怎么解决呢?
限制一些特殊情况的小行为不允许代工行为,在Golang中systemcall和netpoll的处理情况就各不同,systemcall不会存在跨线程执行,它分配在A线程上执行就不会被其他线程接管,而netpoll就可能会被其他线程接管。
通过上下文的包裹和限制,类似实现一个票据,交到外卖员手中,外卖员每次被调度执行之时都会看一下票据来根据优先顺序执行。
那假设我通知完外卖员,我睡着了怎么办?我压根没有接到外卖员回执怎么办?这种情况存在于主线程比协程执行单元更先执行完。
我每通知一个外卖员我就在本子上记下一笔,当我自己做完了自己的事情以后,我在房间里来回踱步,等待外卖员们的回执,回执一个我就划掉一笔。直到划完全部,然后再去睡觉;这就是waitGroup,简单地可以通过一个循环来查询某个计数器判断跳出实现。
PHP如何实现协程方案
PHP常使用多进程,进程间通讯传递信息极为不方便,同时消耗资源会更多,通常来说不会用进程来实现协程的执行单元;但是我们想到异步想到非阻塞,我们就会想到event-loop
,对,我们可以通过event-loop
来实现协程执行单元,将协程执行单元注册在event-loop中来进行执行。
但是要注意的是实现完整的协程方案除了协程、协程执行单元外还需要一个协程调度器,所以在每个event-loop注册执行前后需要实现调度器和调度规则才可以,让event-loop进行合理的回调的中断和继续,合理利用时间。
这样做其实会让event-loop
变得比较臃肿和复杂,不是特别利于维护,整体思路其实和golang
的systemcall
实现方案是相似的,因为都是在同一个线程上进行执行,不存在线程的切换。
简单理解
最后呢,其实你把协程方案想象成是一个缩小的队列系统,由一个程序A发布消息(协程),由一个程序B调度(队列服务),再有另一个程序C进行消费,并在消费后通知来源程序,只不过ABC都是在一个线程或者一个程序内执行罢了,它们只是疯狂利用了时间的空隙罢了。