书接上文:Go的隐秘世界:从 Cgo 到 Goroutine 调度
我们深入 Go 的隐秘世界的旅程就是解释清楚以下几个核心概念之间的逻辑关系的过程。
- CPU core
- thread
- OS scheduler
- goroutine
- Go scheduler
- Cgo calls
很多读者会说”我知道!“ —— Go 程序的高性能来自对 CPU cores 的充分利用。得益于 goroutine。Go runtime 里的 Go scheduler 负责把 goroutine 调度到 thread 上执行。OS scheduler 负责把 thread 调度到 CPU core 上执行。
这里有两个问题:
- thread 还不够支持高性能吗?为啥还需要 goroutine?
- Cgo 和上述 goroutine/thread 有什么关系。
这篇文章着重解释第一个问题。日后再解释第二个。
线程的好处是 —— 一些 I/O 操作因为经常要等磁盘或者网卡,把它们放在一些线程里,这样当这些操作因为等待而”卡住“(blocked)的时候,core 别闲着 —— 可以执行其他没有 block 的线程。
不过,线程切换(context switch,OS scheduler 让一个 core 从执行一个 thread 变为执行另一个 thread 的过程)是有代价的。每次切换耗时大约 1,000 ~ 1,500 纳秒,这些时间本可以用来执行 12,000~18,000 条 CPU 指令!
为什么这么慢?因为 OS scheduler 实现的 context switch 是 preemptive 的(强制的)—— 一个 thread 在一个 core 上执行一段时间后,会被 OS scheduler 强制换成另一个 thread。强制切换的好处是,写程序的人不用考虑切换,不用让自己的程序配合 OS scheduler 来做切换。
因为切换很慢,所以 OS scheduler 尽量不切换。比如如果我们的 CPU 就一个 core,而有两个线程,则必须时常切换 —— 切换耗时,不好。如果我们的 CPU 有两个 core,则不用经常切换,但是往往这两个 core 都用不满 —— 也不好。
为什么用不满?考虑一个经典的例子:比较两个二叉树的前序遍历顺序是否相同。我们可以启动两个个 thread,第一个前序遍历第一个二叉树,每访问到一个节点则把节点输出到一个 blocking queue;第二个处理第二个二叉树,每访问到一个节点,则从 blocking queue 里读一个节点来比较。
这个例子里,两个线程互相等待 —— 第一个线程要等第二个线程从 blocking queue 里把结果拿走,才能写入自己访问的节点。第二个线程要等第一个线程往 blocking queue 里放了一个节点,才能拿出来和自己访问到的节点比较。所以即使我们有 两个 CPU cores,不用切换,这两个 core 也用不满,一个工作时,另一个是闲着的。
切换慢,不切换又用不满 CPU —— 尴尬。所以我们需要协程(coroutine)。Go 实现的 coroutine 叫 goroutine。
协程和线程的区别是,协程的调度方式是 non-preemptive 的 —— 也就是说一个 goroutine 干一段时间后要主动告诉 Go scheduler “俺累了,想歇会儿,换别人吧”。Go scheduler 因此让执行这个 goroutine 的 thread 转而执行另一个 goroutine。正因为 goroutines 有主动放弃执行机会的“协作精神”(collaborative),所以叫 coroutine。
Go 程序员会好奇 —— 我们写程序的时候可从来没有写“告诉 Go scheduler 本 goroutine 累了”这样的 code?这是因为这个“告诉“操作被隐藏在 Go 的标准库函数,以及一些语法操作如 go 和 channel I/O 里了。
用两个 goroutines 而不是两个 threads 来比较二叉树,可以解决上述切换慢,不切换又用不满 CPU 的问题。当 CPU 只有一个 core 时,OS scheduler 只需要启动一个 thread —— Go scheduler 让这个 thread 轮流执行两个 goroutine。这样,虽然两个 goroutine 要互相等待,但是其实是一个 thread 在执行它俩,此时执行这个 thread 的 core 被用满了。
如果系统里有两个甚至多个 core,因为只需要一个 core 和一个 thread 来执行上述两个 goroutines,所以其他 cores 可以被 OS scheduler 用来执行其他程序。
因为 coroutine 的调度是 coroutine 主动通知 Go scheduler 的,所以要做的操作相对 OS scheduler 做 thread context swtich 更简单,coroutine 的 context switch 更快。
接下来,我们看看一个 goroutine 的执行到底要几个 threads。
王益:Go的隐秘世界:一个Goroutine要几个Thread有疑问加站长微信联系(非本文作者)