协程 #
有栈 #
有栈协程和无栈协程根据它们存储上下文的机制来命名
有栈和无栈的不同之一为:==协程是否可以在其任意嵌套函数中被挂起,嵌套函数可理解为 子函数、匿名函数==
对有栈协程来说,通过切换栈帧,改变函数调用栈的方式实现,其实现关键就在于如何保存、恢复和切换上下文。
- 保存上下文:保存从这个函数及其嵌套函数的(连续的)栈帧存储的值,以及此时寄存器存储的值
- 恢复上下文:将这些值重新写入对应的栈帧和寄存器
- 切换上下文:保存当前正在运行的函数的上下文,恢复下一个将运行函数的上下文
goroutine #
GMP
- G: goroutine 每个 go 关键字创建一个协程
- M: thread 内核级线程,所有 G 都在 M 上运行
- P: processor 负责调度
G #
运行时使用私有结构体 runtime.g
表示,每个 goroutine 都持有分别存储 defer
和 panic
等对应结构体的链表
goroutine 在运行时定义的状态非常多且复杂,但是可以将这些状态聚合为三种
- 等待中:正在等待某些条件满足,如系统调用结束等
- 可运行:已准备就绪,可以在线程上运行。如果当前程序中存在非常多的 goroutine,每个 goroutine 就可能会等待更多时间
- 运行中:正在某个线程上运行
M #
操作系统线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用)。最多只会有 GOMAXPROCS
个活跃线程能够正常运行。
在默认情况下,运行时会将 GOMAXPROCS
设置成当前机器的核数,我们也可以在程序中使用 runtime.GOMAXPROCS
来改变最大的活跃线程数。
P #
处理器 P 是线程和 Goroutine 的中间层,它能提供线程需要的上下文环境,也会负责调度线程上的等待队列,通过处理器 P 的调度,每一个内核线程都能够执行多个 Goroutine,它能在 Goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的利用率。
Go 语言程序的处理器数量一定会等于 GOMAXPROCS
,这些处理器会绑定到不同的内核线程上。
无栈 #
无栈协程不改变函数调用栈,采用类似生成器(generator)的思路实现上下文切换。所有栈帧的状态保存在堆上,栈帧内保存的不是状态而是指向状态的指针。
演变: generator(yield) -> future/promise -> async/await
局部数据存储:无栈协程的局部存储通过参数传递实现
协程切换控制权在用户手中,不需要照顾复杂的寄存器状态,不需要上下文切换,因为全局上下文没有改变,只需要改变他们的调用关系
C++20 协程 #
标准只保函编译器需要实现的底层功能
定义包含了以下之一的函数是协程:
- co_await 表达式——用于暂停执行,直到恢复
- co_yield 表达式——用于暂停执行并返回一个值
- co_return 语句——用于完成执行并返回一个值
constexpr、构造函数、析构函数、main 函数不能是协程
协程关联
- promise: 从协程内部操纵。协程通过此对象提交其结果或异常。
- coroutine handle: 从协程外部操纵。这是用于恢复协程执行或销毁协程帧的非拥有柄。
- coroutine state
参考资料: