Go?runtime?調(diào)度器之系統(tǒng)調(diào)用引起的搶占
0. 前言
第八講介紹了當 goroutine 運行時間過長會被搶占的情況。這一講繼續(xù)看 goroutine 執(zhí)行系統(tǒng)調(diào)用時間過長的搶占。
1. 系統(tǒng)調(diào)用時間過長的搶占
看下面的示例:
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 執(zhí)行一個 5s 的系統(tǒng)調(diào)用,在系統(tǒng)調(diào)用過程中,sysmon 會監(jiān)控 longSyscall,發(fā)現(xiàn)執(zhí)行系統(tǒng)調(diào)用過長,會對其搶占。
回到 sysmon 線程看它是怎么搶占系統(tǒng)調(diào)用時間過長的 goroutine 的。
func sysmon() {
...
idle := 0 // how many cycles in succession we had not wokeup somebody
delay := uint32(0)
...
for {
if idle == 0 { // start with 20us sleep...
delay = 20
} else if idle > 50 { // start doubling the sleep after 1ms...
delay *= 2
}
if delay > 10*1000 { // up to 10ms
delay = 10 * 1000
}
usleep(delay)
...
// retake P's blocked in syscalls
// and preempt long running G's
if retake(now) != 0 {
idle = 0
} else {
idle++
}
...
}
}類似于運行時間過長的 goroutine,調(diào)用 retake 進行搶占:
func retake(now int64) uint32 {
n := 0
lock(&allpLock)
for i := 0; i < len(allp); i++ {
pp := allp[i]
if pp == nil {
continue
}
pd := &pp.sysmontick
s := pp.status
sysretake := false
if s == _Prunning || s == _Psyscall { // goroutine 處于 _Prunning 或 _Psyscall 時會搶占
// Preempt G if it's running for too long.
t := int64(pp.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
} else if pd.schedwhen+forcePreemptNS <= now {
// 對于 _Prunning 或者 _Psyscall 運行時間過長的情況,都會進入 preemptone
// preemptone 我們在運行時間過長的搶占中介紹過,它主要設置了 goroutine 的標志位
// 對于處于系統(tǒng)調(diào)用的 goroutine,這么設置并不會搶占。因為線程一直處于系統(tǒng)調(diào)用狀態(tài)
preemptone(pp)
// In case of syscall, preemptone() doesn't
// work, because there is no M wired to P.
sysretake = true
}
}
if s == _Psyscall {
// Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
// P 處于系統(tǒng)調(diào)用之中,需要檢查是否需要搶占
// syscalltick 用于記錄系統(tǒng)調(diào)用的次數(shù),在完成系統(tǒng)調(diào)用之后加 1
t := int64(pp.syscalltick)
if !sysretake && int64(pd.syscalltick) != t {
// pd.syscalltick != pp.syscalltick,說明已經(jīng)不是上次觀察到的系統(tǒng)調(diào)用了,
// 而是另外一次系統(tǒng)調(diào)用,需要重新記錄 tick 和 when 值
pd.syscalltick = uint32(t)
pd.syscallwhen = now
continue
}
// On the one hand we don't want to retake Ps if there is no other work to do,
// but on the other hand we want to retake them eventually
// because they can prevent the sysmon thread from deep sleep.
// 如果滿足下面三個條件的一個則執(zhí)行搶占:
// 1. 線程綁定的本地隊列中有可運行的 goroutine
// 2. 沒有無所事事的 P(表示大家都挺忙的,那就不要執(zhí)行系統(tǒng)調(diào)用那么長時間占資源了)
// 3. 執(zhí)行系統(tǒng)調(diào)用時間超過 10ms 的
if runqempty(pp) && sched.nmspinning.Load()+sched.npidle.Load() > 0 && pd.syscallwhen+10*1000*1000 > now {
continue
}
// 下面是執(zhí)行搶占的邏輯
unlock(&allpLock)
// Need to decrement number of idle locked M's
// (pretending that one more is running) before the CAS.
// Otherwise the M from which we retake can exit the syscall,
// increment nmidle and report deadlock.
incidlelocked(-1)
if atomic.Cas(&pp.status, s, _Pidle) { // 將 P 的狀態(tài)更新為 _Pidle
n++ // 搶占次數(shù) + 1
pp.syscalltick++ // 系統(tǒng)調(diào)用搶占次數(shù) + 1
handoffp(pp) // handoffp 搶占
}
incidlelocked(1)
lock(&allpLock)
}
}
unlock(&allpLock)
return uint32(n)
}進入 handoffp:
// Hands off P from syscall or locked M.
// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func handoffp(pp *p) {
// if it has local work, start it straight away
// 這里如果 P 的本地有工作(goroutine),或者全局有工作的話
// 將 P 和其它線程綁定,其它線程指的是不是執(zhí)行系統(tǒng)調(diào)用的那個線程
// 執(zhí)行系統(tǒng)調(diào)用的線程不需要 P 了,這時候把 P 釋放出來,算是資源的合理利用,相比于線程,P 是有限的
if !runqempty(pp) || sched.runqsize != 0 {
startm(pp, false, false)
return
}
...
// no local work, check that there are no spinning/idle M's,
// otherwise our help is not required
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
}
...
// 判斷全局隊列有沒有工作要處理
if sched.runqsize != 0 {
unlock(&sched.lock)
startm(pp, false, false)
return
}
...
// 如果都沒有工作,那就把 P 放到全局空閑隊列中
pidleput(pp, 0)
unlock(&sched.lock)
}可以看到搶占系統(tǒng)調(diào)用過長的 goroutine,這里搶占的意思是釋放系統(tǒng)調(diào)用線程所綁定的 P,搶占的意思不是不讓線程做系統(tǒng)調(diào)用,而是把 P 釋放出來。(由于前面設置了這個 goroutine 的 stackguard0,類似于 運行時間過長 goroutine 的搶占 的流程還是會走一遍的)。
我們看一個示意圖可以更直觀清晰的了解這個過程:

handoff 結束之后,增加搶占次數(shù) n,retake 返回:
func sysmon() {
...
idle := 0 // how many cycles in succession we had not wokeup somebody
delay := uint32(0)
for {
if idle == 0 { // start with 20us sleep...
delay = 20 // 如果 idle == 0,表示 sysmon 需要打起精神來,要隔 20us 監(jiān)控一次
} else if idle > 50 { // start doubling the sleep after 1ms...
delay *= 2 // 如果 idle 大于 50,表示循環(huán)了 50 次都沒有搶占,sysmon 將加倍休眠,比較空,sysmon 也不浪費資源,先睡一會
}
if delay > 10*1000 { // up to 10ms
delay = 10 * 1000 // 當然,不能無限制睡下去。最大休眠時間設置成 10ms
}
if retake(now) != 0 {
idle = 0 // 有搶占,則 idle = 0,表示 sysmon 要忙起來
} else {
idle++ // 沒有搶占,idle + 1
}
...
}
...
}2. 小結
本講介紹了系統(tǒng)調(diào)用時間過長引起的搶占。下一講將繼續(xù)介紹異步搶占。
到此這篇關于Go runtime 調(diào)度器之系統(tǒng)調(diào)用引起的搶占的文章就介紹到這了,更多相關Go runtime 調(diào)度器 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于Go?goroutine實現(xiàn)一個簡單的聊天服務
對于聊天服務,想必大家都不會陌生,因為在我們的生活中經(jīng)常會用到,本文我們用?Go?并發(fā)來實現(xiàn)一個聊天服務器,這個程序可以讓一些用戶通過服務器向其它所有用戶廣播文本消息,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下2023-06-06
Go中調(diào)用JS代碼(otto)的實現(xiàn)示例
Otto是一個用Go語言實現(xiàn)的JavaScript解釋器,可用于執(zhí)行和操作JavaScript代碼,適合在Go項目中執(zhí)行簡單的JS腳本,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-10-10
Golang并發(fā)繞不開的重要組件之Goroutine詳解
Goroutine、Channel、Context、Sync都是Golang并發(fā)編程中的幾個重要組件,這篇文中主要為大家介紹了Goroutine的相關知識,需要的可以參考一下2023-06-06

