原创文章,欢迎转载,转载请注明出处,谢谢。
第八讲介绍了当goroutine运行时间过长会被抢占的情况。本文将继续探讨goroutine执行系统调用时间过长的抢占机制。
看下面的示例:
func longSyscall() {
timeout := syscall.NsecToTimeval(int64(5 * time.Second))
fds := make([]syscall.FdSet, 1)
if _, err := syscall.Select(0, &fds[0], nil, nil, &timeout); err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Select returned after timeout")
}
func main() {
threads := runtime.GOMAXPROCS(0)
for i := 0; i < threads; i++ {
go longSyscall()
}
time.Sleep(8 * time.Second)
}
longSyscall goroutine执行一个5秒的系统调用。在系统调用过程中,sysmon会监控longSyscall,发现执行系统调用时间过长,会对其进行抢占。
sysmon线程会监控并抢占系统调用时间过长的goroutine。它的抢占逻辑如下:
func sysmon() {
...
idle := 0 // 连续多少个周期没有唤醒其他goroutine
delay := uint32(0)
...
for {
if idle == 0 { // 从20微秒的休眠开始…
delay = 20
} else if idle > 50 { // 1毫秒后开始加倍休眠…
delay *= 2
}
if delay > 10*1000 { // 最多10毫秒
delay = 10 * 1000
}
usleep(delay)
...
// 重新获取处于系统调用的P
// 并抢占运行时间过长的G
if retake(now) != 0 {
idle = 0
} else {
idle++
}
...
}
}
类似于运行时间过长的goroutine,调用retake进行抢占。retake函数用于抢占处于_Prunning或_Psyscall状态的goroutine。对于系统调用时间过长的goroutine,也会进行抢占。
进入handoffp:
// 从系统调用或锁定的M中释放P
// 始终在没有P的情况下运行,因此不允许写屏障。
//
//go:nowritebarrierrec
func handoffp(pp *p) {
// 如果P的本地队列有工作,立即开始工作
// 如果P的本地队列有工作或全局队列有工作,则将P与其他线程绑定,以释放P
if !runqempty(pp) || sched.runqsize != 0 {
startm(pp, false, false)
return
}
...
// 没有本地工作,检查是否有自旋/空闲的M
// 如果有,则不需要帮助
if sched.nmspinning.Load()+sched.npidle.Load() == 0 && sched.nmspinning.CompareAndSwap(0, 1) { // TODO: fast atomic
sched.needspinning.Store(0)
startm(pp, true, false)
return
}
...
// 如果全局队列有工作,则释放P
if sched.runqsize != 0 {
unlock(&sched.lock)
startm(pp, false, false)
return
}
...
// 如果没有工作,则将P放入全局空闲队列
pidleput(pp, 0)
unlock(&sched.lock)
}
可以看到,抢占系统调用时间过长的goroutine,抢占的意思是释放系统调用线程所绑定的P,而不是阻止线程执行系统调用。抢占的目的是合理利用P资源。抢占完成后,增加抢占次数n,retake函数返回。
本文介绍了系统调用时间过长引起的抢占机制。下一篇文章将继续介绍异步抢占。