Go语言sysmon监控线程的用法(附带实例)
Go语言中,当触发 gopark() 函数(用于将 G 挂起)、锁、通道、网络、垃圾回收、sleep 等相关操作时,G 的状态会从 _Grunning 变为 _Gwaiting,且会进入对应的定时器中等待。
每个 P 都有一个用于管理自己的定时器字段 p.timers,定时器中有对应的回调函数 checkTimers。每次调度 G 时都会执行该函数,检查并执行已经到时间的定时器。在到达指定的时间后调用这个回调函数,会让等待的 G 又恢复到 _Grunnable 状态,这时再将其重新放回到本地运行队列中。
但如果当前所有的 M 都处于运行状态,那么就可能存在不能及时触发调度的情况,最终会导致定时器不能被及时地执行。为了解决这个问题,Go 语言引入了监控线程 sysmon,它不与任何 P 绑定,直接由 M 执行。
监控线程 sysmon 也是由主协程创建的,其在 runtime 初始化之后,执行用户代码之前,由 runtime 启动。关键代码如下:
sysmon 用于监控系统的运行状态,并在出现意外情况时及时做出响应。由于它需要重复执行任务,因此始终处于运行状态。
在程序执行期间,监控线程 sysmon 每 20us~10ms 轮询一次,以监控长时间运行的 G。若发现有长时间运行的 G,就对其设置可被抢占的标识符,以便其他的 G 抢占执行。关键代码如下:
如果遇到网络请求导致运行阻塞的情况,调度器会将当前被阻塞的 G 放入网络轮询器中。这时,网络轮询器会执行异步网络系统调用,并让出 P,以便 P 能执行其他 G。
待网络轮询器完成异步网络调用后,将由监控线程 sysmon 将 G 切换回来并继续运行。关键代码如下:
如果是系统调用引起的阻塞(P 的状态为 _Psyscall),则触发分离机制。当一个 P 处于 _Psyscall 状态时,调度器会把这个 P 从其所绑定的 M 上分离下来,并重新寻找其他空闲的 M 来执行相应的 G 代码。当系统调用返回时,调度器会再次把这个 P 绑定到一个空闲的 M 上,让它继续执行。
retake() 函数就是用来实现这个分离机制的。当调度器发现一个 P 的状态为 _Psyscall 时,就会调用 retake() 函数来重新获取这个 P,并把它绑定到一个空闲的 M 上执行。这样,就可以避免出现因系统调用阻塞导致的 G 无法执行的问题。关键代码如下:
如果垃圾回收器已经有两分钟没有运行了,则监控线程 sysmon 会通知 gchelper 协程强制执行一次垃圾回收。关键代码如下:
每个 P 都有一个用于管理自己的定时器字段 p.timers,定时器中有对应的回调函数 checkTimers。每次调度 G 时都会执行该函数,检查并执行已经到时间的定时器。在到达指定的时间后调用这个回调函数,会让等待的 G 又恢复到 _Grunnable 状态,这时再将其重新放回到本地运行队列中。
但如果当前所有的 M 都处于运行状态,那么就可能存在不能及时触发调度的情况,最终会导致定时器不能被及时地执行。为了解决这个问题,Go 语言引入了监控线程 sysmon,它不与任何 P 绑定,直接由 M 执行。
监控线程 sysmon 也是由主协程创建的,其在 runtime 初始化之后,执行用户代码之前,由 runtime 启动。关键代码如下:
func main() { ... if GOARCH != "wasm" { systemstack(func() { newm(sysmon, nil, -1) }) } ... }
sysmon 用于监控系统的运行状态,并在出现意外情况时及时做出响应。由于它需要重复执行任务,因此始终处于运行状态。
在程序执行期间,监控线程 sysmon 每 20us~10ms 轮询一次,以监控长时间运行的 G。若发现有长时间运行的 G,就对其设置可被抢占的标识符,以便其他的 G 抢占执行。关键代码如下:
for { if idle == 0 { delay = 20 } else if idle > 50 { delay *= 2 } if delay > 10*1000 { delay = 10 * 1000 } usleep(delay) ... }
如果遇到网络请求导致运行阻塞的情况,调度器会将当前被阻塞的 G 放入网络轮询器中。这时,网络轮询器会执行异步网络系统调用,并让出 P,以便 P 能执行其他 G。
待网络轮询器完成异步网络调用后,将由监控线程 sysmon 将 G 切换回来并继续运行。关键代码如下:
for { ... lastpoll := int64(atomic.Load64(&sched.lastpoll)) if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now { atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now)) list := netpoll(0) if !list.empty() { incidlelocked(-1) injectglist(&list) incidlelocked(1) } } ... } ... }
如果是系统调用引起的阻塞(P 的状态为 _Psyscall),则触发分离机制。当一个 P 处于 _Psyscall 状态时,调度器会把这个 P 从其所绑定的 M 上分离下来,并重新寻找其他空闲的 M 来执行相应的 G 代码。当系统调用返回时,调度器会再次把这个 P 绑定到一个空闲的 M 上,让它继续执行。
retake() 函数就是用来实现这个分离机制的。当调度器发现一个 P 的状态为 _Psyscall 时,就会调用 retake() 函数来重新获取这个 P,并把它绑定到一个空闲的 M 上执行。这样,就可以避免出现因系统调用阻塞导致的 G 无法执行的问题。关键代码如下:
for { ... if retake(now) != 0 { idle = 0 } else { idle++ } ... }
如果垃圾回收器已经有两分钟没有运行了,则监控线程 sysmon 会通知 gchelper 协程强制执行一次垃圾回收。关键代码如下:
for { ... if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 { lock(&forcegc.lock) forcegc.idle = 0 var list gList list.push(forcegc.g) injectglist(&list) unlock(&forcegc.lock) } ... }