先澄清几个问题:

  1. 什么是协程 ( Coroutine )?

协程可以主动放弃 CPU 使用权并交给约定的另外一个协程,根据约定方式的差异——明确指定跳转到另一个协程 或者 交还给调用者(另一个协程)——可分为 非对称(两种方式都可以) 和 对称协程(只允许交还 CPU 给调用者) 两种。但这种区分方法并不一定就是业界共识,只是有论文提出过这种概念。

抛开协程的物理实现方式不谈(即不讨论栈帧和寄存器之类的概念),协程必然存在一个执行上下文的概念。协程切换前后,其执行上下文是不变的,就好像这个切换没有发生过一样。这一点和 线程切换是一样的。

从这个概念来看,以我所知,goroutine 并不是 coroutine 协程。

因为实际上程序员并不能自行指定切换到哪一个 goroutine,而是由 gosched 来自行决定下一个要从 suspend 变成 active 的 goroutine。

但 goroutine 也不能说是抢占式的 (preemptive),因为 goroutine 被切换的时机是明确的,就是访问 chan 等等应该 block 的时候。

  1. 协程的实现方式及代价

执行上下文的这个概念,对应到物理实现方式的时候,有很多种实现方式。

C# yield return 搭配 IEnumeratable 语法糖 和 async await 的实现方式是,在用户代码之中插入状态机代码和数据,使得从程序员的角度看来是保持了上下文不变。这是编译器魔法,是编程语言相关的。

Windows Fiber API 以及boost::fiber boost::coroutine 的实现方式是保存寄存器状态和栈帧数据。这实际上就是通用 内核 实现 进程切换的 技术变种(所以实现方式是平台相关的),可以称为 平台魔法

这两种魔法跟线程切换的最大区别就是无需系统内核介入( windows fiber 实际上应该不需要深入内核,但是不是真的没有进入内核,我并没有研究)。因此,假设在同一个 OS,同一个 CPU 满负载都用于协程/线程的情况下,支持发生协程切换的最大次数,很大可能是高于线程切换的。

但是这个数据对实践并没有什么指导意义。因为实际生产环境中很少能把 CPU 合理地用满。

两种实现方式都需要额外都内存来存储上下文,只不过编译器魔法保存上下文的内存使用概率可能高一点(因为明确知道上下文都数据大小)但是会丧失调用栈上下文的信息,而平台魔法的上下文数据通常是要预先分配(通常会过量分配)。

=================== 我的理解 线程本身是操作系统概念,为了解决进程切换的高代价来实现的。 后来发现线程切换也不是很完美,那么就有了『用户态线程』以及『协程』,这两个我不是很区分有什么不同,但是确定的是都是在用户态直接切换的,比系统线程轻量,不需要进入内核等。 而通常意义上的协程例如 lua,切换是手动的或者确定的,你必须用 yield/await 来控制。但是 golang 里,你只需要把 goroutine 扔给 golang 的调度器,它自然帮你干好这些事情。实际上调度器开启了 N 个线程来分配 goroutine,也就是通常说的 M:N,比起手动控制,只简化为一种方式:后台运行,通过 channel 沟通,大大简化了我等程序员的逻辑负担……

===================

c# yeild return

Methods which use yield return are coroutines - in each foreach iteration they execute up to the next yield statement they find and then they exit, picking up where they left off in the next iteration.

Internals. The C# code you have that uses yield is never actually executed by the CLR at runtime. Instead, the C# compiler transforms that code before the runtime ever occurs.

foreach

With a foreach loop, we evaluate each element individually. An index is not needed. With no indexes, loops are easier to write and programs are simpler.