在 Google I/O 2012 演示Go Concurrency Patterns中,Rob Pike 提到多个goroutine可以存在于一个线程中。这是否意味着它们被实现为协程?如果没有,它们是如何实施的?欢迎提供源代码链接。
4 回答
IMO,协程意味着支持将控制权转移到另一个协程的显式方法。也就是说,程序员在决定协程何时应该暂停执行并将其控制权传递给另一个协程(通过调用它或通过返回/退出(通常称为屈服))时以某种方式对协程进行编程。
Go 的“goroutines”是另一回事:它们在某些不确定的点1隐式放弃控制,当 goroutine 将要在某些(外部)资源上休眠时发生,例如 I/O 完成、通道发送等。这种方法与通过通道共享状态相结合使程序员能够将程序逻辑编写为一组顺序的轻量级进程,从而消除了协程和基于事件的方法常见的意大利面条代码问题。
关于实现,我认为它们与(不幸的是不太知名)“状态线程”库非常相似,只是级别较低(因为 Go 不依赖libc
或类似的东西并直接与操作系统对话kernel) — 你可以阅读 ST 库的介绍性论文,其中的概念得到了很好的解释。
1事实上,这些点不如协程确定,但比在抢占式多任务下的真正操作系统线程更确定,其中每个线程都可能在任何给定时间点和线程控制流中被内核挂起。
2021-05-28 更新:实际上,从 Go 1.14 开始,goroutines 被(几乎)抢占式调度。不过应该注意的是,典型内核对其管理的线程进行的硬核抢占仍然没有那么大,但它比以前更接近了;至少,一旦进入繁忙循环,goroutine 现在就不可能变成不可抢占的了。
不完全的。Go 常见问题解答部分为什么使用 goroutines 而不是线程?解释:
Goroutines 是使并发易于使用的一部分。这个想法已经存在了一段时间,它是将独立执行的函数(协程)多路复用到一组线程上。当协程阻塞时,例如通过调用阻塞系统调用,运行时会自动将同一操作系统线程上的其他协程移动到不同的可运行线程,这样它们就不会被阻塞。程序员看不到这些,这就是重点。结果,我们称之为 goroutine,可能非常便宜:除了堆栈内存之外,它们几乎没有开销,只有几千字节。
为了使堆栈变小,Go 的运行时使用可调整大小的有界堆栈。一个新创建的 goroutine 有几千字节,这几乎总是足够的。如果不是,则运行时会自动增长(和缩小)用于存储堆栈的内存,从而允许许多 goroutines 存在于适度的内存中。CPU 开销平均每个函数调用大约三个廉价指令。在同一个地址空间中创建数十万个 goroutine 是很实用的。如果 goroutines 只是线程,系统资源会以更少的数量耗尽。
在https://groups.google.com/forum/?fromgroups=#!forum/golang-nuts上经常讨论 goroutine 是适当的协程还是类似的东西。有些人可能会争论这样的微妙之处,但对于大多数人来说:goroutine 是协程。
查看https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit 以了解调度程序的工作原理。