Skip to content

Latest commit

 

History

History
57 lines (34 loc) · 4.53 KB

File metadata and controls

57 lines (34 loc) · 4.53 KB

协程 (coroutine)

以下观点仅代表个人见解

协程这个概念, 目前最流行的恐怕就是goroutine了. 有些文章一说到协程, 张口就是绿色线程, 用户态线程, 轻量级线程, 却没解释概念, 也没说实际上是否并行等等其实是取决于具体实现的.

昨晚在知乎看到一篇回答, 有种茅塞顿开的感觉, 我也十分认同他所说的.

我们先说协程的概念上的意思, 协程其实就是一个可主动中断的过程,函数. 在用户空间里实现的, 对内核是不可见的, 运行在线程上. 它也不能直接避免阻塞.

说到阻塞, 我认为一般有io阻塞和计算(cpu)阻塞. 概念上的协程并不能实现非阻塞, 比如你在一个协程调用了一个read方法, 实际上这个read是用syscall调用内核的接口, 会阻塞整个线程, 也就意味着阻塞整个线程上全部的协程.

那么主流语言上的异步, 协程等等是怎么实现非阻塞io的呢, 比如rust的tokio框架, 底层是用mio库, mio是对底层操作系统非阻塞io的抽象, 比如epoll, iocp等等. 而php的amphp框架则是用了php标准库里的非阻塞io函数stream_set_blocking等等.

回到协程的概念, 协程实现非阻塞io, 实际上就是封装了非阻塞io方法, 在io还没有准备好的时候主动去中断, 切换到下一个协程. 直到执行权又一次回到自己, 再检查一次io是否准备好, 如果还没有, 那么继续上面操作.

所以你能看见, 在tokio和amphp这些第三方的异步框架, 都是需要用框架本身提供的封装好的非阻塞api, 而不能直接用std提供的阻塞api.(除非hook了标准库api).

但是你会发现golang是不需要另外区分阻塞和非阻塞api的, 这就跟具体实现有关了.

go我不太了解, goroutine是语言层面实现的, 据其他人所说, goroutine底层是抢占式调度的, 线程池, 所以即使是阻塞io也不会阻塞整个程序. 而且由于是语言层面实现的, 标准库里io api应该对goroutine有特殊优化吧.

而tokio是基于rust的future和async/await来实现的, 有多线程模式, 在多线程上跑多个task, 可以实现任务窃取. 一个task类似一个协程, 但据我所知tokio并不是抢占式调度, 所以在task里非异步的阻塞可能会阻塞整个线程和线程上的全部task.

所以goroutine和tokio多线程模式都是可以 并行 的.

php的amphp是一个纯php实现的异步框架, 同类型的还会有reactphp和用c写的swoole. amphp是用generator(生成器)/yield来实现的异步, 生成器是没有堆栈的, 类似无栈协程, 通过yield来主动中断. 而在php8.1, 以amphp社区为首大力推动的ext-fiber会合并到php-src里, 这意味着php不需要拓展就能支持fiber, 并且amphp v3也使用了fiber.

但是php是单线程的, 所以除了用c实现的swoole以外, 纯php实现的异步框架都是不支持 并行 的.

php的fiber

fiber 又称(纤程), 实际上跟协程是一样的东西不同名字. 那么在php里的fiber跟用generator/yield有什么区别呢?

首先, fiber是有栈的, 并且不会污染整个调用栈. 因为yield这套, 跟rust的async/await一样, 是会传染/污染的, 也就是说, 你可以在异步函数里用同步函数, 但是不能在同步函数里用异步函数, 一旦你返回了一个promise/future, 那么整个调用栈都需要返回一个promise, 直到你扔到执行器为止.(上面那句话可能有点绕, 但是你上手写一下就知道什么意思了).

而php的fiber也是比较轻量的, fiber的栈大小是可以动态增长的, 最开始只有4k, 默认最大8m, 可以手动配置stack size.


随便吐槽一下amphp, amphp的parallel库醒目写着"使用多进程或者线程为php提供真正的并行处理, 无需拓展". 但是实际上你还是得装ext-parallel或者ext-pthreads来支持多线程, 否则纯php只能用多进程来并行. 颇有标题党的味道(

amphp的非阻塞文件io也是得用libuv/libevent等等拓展, 否则回退到同步或者多进程....没错, 多进程😅

协同式调度和抢占式调度

假设有甲乙丙在等待调度, 甲运行需要10ms, 乙20ms, 丙1ms.

顾名思义, 协同调度就是需要等待甲10ms, 然后乙20ms, 才轮到甲, 若是甲不小心化了50ms, 不好意思后面等着.

而抢占调度, 则是在一定的时间内, 假设15ms, 甲花了10ms, 轮到乙花了15ms还没跑完的时候, 就会切换到下一个(这里应该有"拿来吧你".jpg).

并且抢占式调度是有优先级的, 简称人上..呸, 高优先级的会比低优先级的更快轮到.