全文私货,没一点干的:
协程就是协作式多任务,你可以把协作式用户态多线程,“有栈协程”称为协程,也可以把cps模拟的,或者加了糖的cps模拟的(js:你再骂???)协作式多线程称为协程,或者把基于异步运行时的async-await称为协程。
免责声明:js(现在)的promise和async-await不是cps模拟(虽然它可以是)
协程本质的特点是什么呢,是任务可以“让出”执行权,之后在合适的时机可以恢复执行。而如果任务不让出执行权,那么它便不会打断:这便是“协作式”的含义。
简单说就是“有让出无抢占”,就叫协程。
比如yield。实际上抢占式多线程本来就可以通过yield主动让出执行权,只不过抢占式多线程无法阻止自己被抢占。
await也是让出执行权的方式。
我们看到抢占式多线程减去抢占就等于协程,但是因为直接使用内核态多线程是不能阻止抢占的,所以这个思路实现的协程基本上都是内核线程池加协作式用户态线程。
为什么协程相比线程具有高性能,这不是因为上下文切换,协程同样需要上下文切换,用户态线程的上下文切换同样不走内核。协程高效的原因之一是,主动进行切换比通过抢占随时可能发生的切换需要保存更少的现场,之二是协程运行时无需实现抢占,就比如你不用timer,不用太多signal handler,这样的协程运行时岂不是好实现多了。
抢占是一个很低效的操作,“打断”这种操作不是那么容易做的,操作系统级别上之所以需要抢占是为了避免任务占着CPU不走。在多处理系统上这个问题实际上可以得到缓解,比如把bsp特供给内核,内核发现哪个任务画风不太对一个ipi过去就好了:不过这种方式显然比抢占更低效。
协程的一些好处也和这个协作式/无抢占有关。比如同步和竞争条件的问题。。。当然M比N的话那就没这回事了:不过避免抢占以提高性能还是一定的,因为你自己知根知底的代码还要去抢占,这基本上就是牺牲吞吐量换取响应时间的操作。
常见的协程实现的区别在调度上,或者更大了说在运行时。
比如用户态多线程运行时本身就有一个调度器,这个调度器会把就绪的协程池在内核线程池中分配。当然如果这个内核线程池不止一个。。。那么甚至可能有另一个协程在另一个核心上和你一起在运行,惊喜不惊喜(
异步运行时比如事件循环,它在实现方看来和用户态多线程没有太大区别,用户态多线程运行时调度的是就绪的线程,事件循环调度的是就绪的事件,也都差不多。如果运行时决定在多个核心上各跑一个事件循环,那么。。。
还有那么一种呢,它没有调度器,它需要用户自己来调度,你什么时候想让一个已经yield掉的协程恢复运行,你要自己去调用它的后续,这就,很有意思也很有用。
还有那么一种,它叫lua,它的协程是使用用户态线程实现的,但是它没有提供调度器,所以它是第三种。(提协程怎么能不提lua呢)
你要问哪个是真协程,怎么说呢,都是,反正我最喜欢无调度器的实现。
csharp的yield,运行时不创建额外的环境,那么yield直接返回到“直接”调用者,直接调用者可以级联yield返回到最终调用者,yield会将当前执行状态,或者可以看作continuation以某种方式保存,恢复时由外界手动调用continuation进行恢复。类似的还有js的generator,c++ 20的协程,这种协程实现对运行时无假设并且提供最细粒度的控制。
lua的coroutine,运行时需要创建用户态线程,yield会中断整个用户态线程返回到线程创建者,yield会将执行状态保存在线程上下文中,恢复由外界手动恢复该线程。这里的协程实现要求yield能够中断整个线程,这可能会引入一定的同步问题,因为上层调用者无法控制下层的yield。所以无栈协程虽然被认为具有传染性,但是我认为这种传染性反而是必要的。
然后async-await,通过await让出执行并返回到直接调用者,直接调用者可以级联await返回到最终调用者,await依然可以看作将运行状态保存为continuation,但是协程的恢复由异步运行时控制,await会将continuation传递给被等待的信号并最终传递给运行时,而非调用者。
yield想要模拟async-await实际上是可行的,即协程在yield的同时给出它想要等待的信号量,调用者在信号量改变后恢复协程执行,这等于手动实现了一个异步运行时。
有调度的有栈协程就是goroutine(的无抢占版),约等于用户态线程去掉抢占。
“任何异步操作都会有一个隐藏在背后的线程或是内核在帮你做事情”
这就不对了,同步操作才是内核在帮你做事。调用同步IO的API之后内核需要帮你把线程改成等待态,等IO完成之后内核再给你改回就绪态。而异步IO呢,内核做完了直接返回,IO结束给你发一个sigio就完了。
只能说操作系统的设计者在这个地方没想明白,异步IO对内核反而是更爽的。
不过,posix嘛。
当然epoll的确是内核在帮你做事了。
先说明一下概念:协程的核心本质是用户态(应用代码)主动释放和获得cpu计算资源的一种架构——区别于传统内核级线程/进程进行上下文切换时,应用层是不可控且无感知的。所以,最极端的情况来说,在代码里到处加sleep(1),也可以算是实现了一个极为粗放的协程。
然后来说一下你的困惑:
协程本来是指由编程语言自身负责调度任务,不借助操作系统与处理器功能的任务调度方式。某种意义上,协程的本质就是「非抢占式多任务」,它当且仅当任务主动让出控制权时进行调度。
这种调度从操作系统看起来其实就是同一个线程,只不过内部实现了「非抢占式多任务」,换句话说同时只能运行一个协程,其它协程只能等待,当前协程需要等待的时候主动把控制权让出来,让其它协程工作,这样就可以允许同一个线程内跑多个协程。
由于协程并不借助操作系统线程机制,所以开销比线程更小。当年的C10K问题最终的解决方案本质上就是协程思路,虽然并不都是用协程实现,但那些异步框架本质上就是一种手动协程的表现形式,使用epoll之类的机制相当于在同一个线程内跑多任务。所以协程思想提高性能是肯定的,毕竟它解决了C10K这个经典问题。
至于说协程减少上下文切换,这是当然的,因为都是同一个线程,同一个线程内当然减少了上下文切换。
Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程,如果你创建了四个协程会发现程序实际运行在四个线程而不是一个线程上。破坏了协程的定义。
当然,这样做反而有好处,毕竟能够更好的利用多核CPU。
如果你CPU是8核的,你创建100个Kotlin协程实际上也就只是用了8个线程。显然Kotlin协程也依然支持多个协程在同一个线程里边跑,在同一个线程内跑的多个协程看起来算是真协程,只不过由于Kotlin协程会优先把CPU处理器核心占满,那么在协程数不多的时候它被分配到线程中去,于是就退化成了线程,看起来就像是个假协程库了。