先说说官话。
线程,有时被称为轻量级进程,是操作系统调度与CPU执行的最小单位。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
看到线程与协程这里你是否有点迷?其实最主要的是需要理解何为用户态、何为内核。用户态即为我们应用程序所存在的空间,而内核态存在于操作系统层。
简而言之,协程也就是我们应用程序模拟实现的线程罢了。
再来谈谈传统线程的缺点
我们的应用程序每创建一个线程,需要发起一次系统调用,这是一次用户态到内核态的切换,并且由于CPU调度的最小单位是线程,如果创建过多的线程会造成调度时CPU时间片白白的浪费。
而Golang的机智之处在于go程序都有一个环境变量作为虚拟核心数设置,即GOMAXPROCS,它默认设置的值与你CPU核心数相同。
因此,我们在Go程序中每创建的Goroutine其实都是基于GOMAXPROCS之上的。
例如:我的电脑是4核,当我们创建了100个Goroutine时,那么这100个协程其实也只是真正的运行在4个核心之上。而100个协程之间的调度是由用户态应用程序自行控制的,它大大减少了从用户态和内核态之间的不停切换,因此加快了效率.......良好的压榨了CPU。并且由于没有过多的创建线程,因此它减少了CPU在线程之间的调度造成的时间片浪费。
而 Golang 内部的协程调度模型之GMP调度模型也便成为了面试官常考项。
再来谈谈协程调度
首先是runtime
多个 goroutine 好比平行宇宙中的每个世界,而 chan 则是 一个穿梭机,方便你在平行世界穿梭,在 go 的空间内,runtime 是上帝,主宰一切。所有对操作系统 API 的调度,都会被 runtime 层拦截以便达到高效调度。
用户态协程调度机制
- N:1 多个用户态协程运行在一个 OS 线程上 (这里上文就讲过)
- 1:1 一个用户态协程对应一个 OS 线程
- M:N 任意数量的用户态协程可以运行在任意数量的 OS 线程上
goroutine 模型(GMP调度模型)
- G:Goroutine golang 协程
- P:Context M 与 P 的中介(有的文章也叫它调度器,它是实现 M:N 模型的关键)
- M:系统线程
且M 必须拿到 P 才能够对 G 进行调度
它大概长下面这个样子

Goroutine调度时机
Goroutine 在 system call 和 channel call 时都可能发生阻塞,当程序发生 system call,M 会发生阻塞,同时唤起(或创建)一个新的 M 继续执行该队列的剩余 G
当程序发起一个 channel call,程序可能会阻塞,但不会阻塞 M,G 的状态会设置为 waiting,M 继续执行其他的 G, 当 G 的调用完成,会有一个可用的 M 继续执行它。
Goroutine上下文切换
上面的也都是官话,一切操作都只是为了压榨 CPU,谁让它那么快,又出现多核。
出现阻塞,意味 CPU 在当前执行域没活干了,它在干等,换 go 而言,调度器要 goroutine 上下文切换
chan 读写出现阻塞时,runtime 会隐式地进行上下文切换,而在极个别情况下,需要程序员显式编码操作,如下所示
至于切换到哪个 goroutine,由调度器决定。但可以肯定的是对同一 chan 所相关的 goroutine 执行有序
1 2 |
// 显式交给调度器切换,否则有的goroutine就饿死了 runtime.Gosched() |
最后来见证 Goroutine vs Python yield
编程语言都是基于操作系统,显然 goroutine 能做的,其它 语言也可以。
只不过,比之于 java,python 之类上古语言,go 做的更多而已。

因此 Golang的协程比Python的强大之处就是在于它的协程能利用多核。
本文参考:https://learnku.com/articles/35045
日常摘录,完。
文章评论(0)