goroutine 是 go 语言的协程, go 语言在语言和编译器层面提供对协程的支持. goroutine 跟线程一个很大区别就是线程是操作系统的对象, 而 goroutine 是应用层实现的线程. goroutine 实际上是运行在线程池上的, 由 go 的 runtime 实现调度, goroutine 调度时, 由于不需要像线程一样涉及到系统调用, 要进行用户态和内核态的切换, 因此, goroutine 被称为轻量级的线程, 开销要比线程小很多. 然而, 这里我想到了一个问题, 线程是由操作系统进行调度的, 操作系统有对处理器的调度权限, 因此线程在上下文切换时, 操作系统可以从正在占用处理器的线程手中剥夺处理器的使用权, 然而 goroutine 该怎么完成这个操作呢?
然而 goroutine 并不能像线程的调度那样, goroutine 调度时, 必须由当前正在占用 CPU 的 goroutine 主动让出 CPU 给新的 goroutine, 才能完成切换操作.
具体实现是这样的, go 对所有的系统调用进行了封装, 当前执行的 goroutine 如果正在执行系统调用或者可能会导致当前 goroutine 阻塞的操作时, runtime 就会把当前这个 goroutine 切换掉. 因此一个很有意思的事情就发生了, 如果当前的 goroutine 没有出现上述的可能会导致 goroutine 切换的条件时, 就可以一直占用 CPU(实际上只是一直占用线程), 而且并不会因为这个 goroutine 占用时间太长而进行切换. 我们可以通过如下这段代码进行验证:
- package main
- import (
- "fmt"
- "sync"
- )
- func process(id int) {
- fmt.Printf("id: %d\n", id)
- for {
- }
- }
- func main() {
- var wg sync.WaitGroup
- n := 10
- wg.Add(n)
- for i := 0; i <n; i++ {
- go process(i)
- }
- wg.Wait()
- }
这段代码输出如下:
- id: 9
- id: 5
- id: 6
- id: 0
按照正常的逻辑, 这段代码应该会输出 0 到 9 一共十个 id, 然而执行后发现, 只输出了四个 (GOMAXPROCS: goroutine 底层线程池最大线程数, 默认为硬件线程数)id, 这就说明实际只有四个 goroutine 得到了 CPU, 而且没有进行切换, 因为 process 这个方法里面没有会导致 goroutine 切换的条件. 然后我们在 for 循环里面加入一个操作, 例如 time.Sleep() 或者 make 分配内存等等
- package main
- import (
- "fmt"
- "sync"
- "time"
- )
- func process(id int) {
- fmt.Printf("id: %d\n", id)
- for {
- time.Sleep(time.Second)
- }
- }
- func main() {
- var wg sync.WaitGroup
- n := 10
- wg.Add(n)
- for i := 0; i < n; i++ {
- go process(i)
- }
- wg.Wait()
- }
- Output:
- id: 2
- id: 0
- id: 1
- id: 9
- id: 6
- id: 3
- id: 7
- id: 8
- id: 5
- id: 4
可以看到这次的输出就是我们预料的结果了. 在知道 goroutine 的调度策略之后, 可以想到这种策略可能会带来的问题, 假如有 n 个 goroutine 出现阻塞, 并且 n>= GOMAXPROCS 时, 将会导致整个程序阻塞.
然而这个问题是无法从根本上解决的, 所以 go 给我们提供了一个方法 runtime.Gosched(), 调用这个方法可以让当前的 goroutine 主动让出 CPU, 这也不失为一个弥补的好方法了. 而且在对 go 程序性能调优的时候, 我们可以根据实际情况来调整 GOMAXPROCS 的值, 例如当有密集的 IO 操作时, 尽量把这个值设置大一点, 可以避免由于大量 IO 操作导致阻塞线程.
以上内容纯属原创, 如有问题欢迎指正!
来源: https://www.cnblogs.com/xiaxiaosheng/p/11190743.html