0%

go 调度器是部分抢占式的以及如何调度

go 调度器是部分抢占式的以及如何调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"
import "runtime"
import "time"

func cpuIntensive(p *int) {
for i := 1; i <= 100000000000; i++ {
*p = i
}
}

func main() {
runtime.GOMAXPROCS(1)

x := 0
go cpuIntensive(&x)

time.Sleep(100 * time.Millisecond)

// printed only after cpuIntensive is completely finished
fmt.Printf("x = %d.\n", x)
}

执行上述代码, print只在cpuIntensive执行完毕后才打印. 除非你在cpuIntensive中手动加入了runtime.Gosched(). 原因是密集的cpu计算过程并不包含可以抢占的抢占点(IO阻塞/垃圾回收等), 实际使用场景中很少会有这种, 但是如果你遇到这样的事情, 就需要手动的添加一些可以抢占的点, 手动除非调度器执行调度.

如果一个goroutine执行过程中到达了go中定义的可以抢占的点(IO/垃圾回收/等待输入等), 那么就会让出执行权给在同一个thread或全局goroutine队列中goroutine, 这个过程对于操作系统层面看是无感知的.切换的代价很小, 只需要变更3个寄存器的值,因此很轻量. go 多路复用thread线程, 这种实现不要求开发者自己去显示地通过事件循环和回调来处理futures/promises. 这种显示的处理需要是在同一个thread中手动创建事件循环,并利用操作系统提供的事件描述符与回调机制做处理.

go中block的goroutine会调用对应的操作系统内核提供的多路并发机制(epoll等)来等待执行结果的返回并同时让出当前线程的执行权限给其他goroutine.但是如果系统不支持IO多路复用,go会创建一个新的线程会去执行其他goroutine, 并等待到block结束后,重新返回这个goroutine,这时可能会存在多于cpu数量GOMAXPROCS的内核线程,这时就不受GOMAXPROCS控制了.

1
2
3
4
evport -> soloris   O(1)
epoll -> linux O(1)
kequeue -> OS X和freeBSD O(1)
select -> 所有系统平台 O(n) 最大支持1024个事件描述符

goroutine建立在事件驱动的架构之上. 同时并发的事件数量受到进程可以同时打开的文件描述符数量限制. 因此ulimit -n 必须设置一个合理值以供在进行大量同时的IO处理使用. 由于IO事件返回通常有一个超时设定, 返回后的文件描述符就会可被其他并发的事件使用, 因此一个较大的nofile值就足够cover大量的并发事件.

对于并发请求服务时,还需要考虑服务端是否有并发限制, 所以同时执行的goroutine数量还需要有一定的考虑

参考 https://hadar.gr/2017/lightweight-goroutines