引入排程器原因
1. go垃圾回收
垃圾回收需要記憶體處于一緻性狀态,需要stop the world,時間點不确定,僅OS排程無法控制。
2. 提高并發性
采用M:N線程模型,每個使用者線程對應多個核心空間線程,同時也可以一個核心空間線程對應多個使用者空間線程。當一個Goroutine在進行阻塞操作(比如系統調用)時,可以把目前線程中的其他Goroutine移交到其他線程中繼續執行, 進而避免了整個程式的阻塞。
3. 更好的管理goroutine
因為引入Goroutine,一個Goroutine既包含要執行的代碼,又包含用于執行該代碼的棧和PC、SP指針等,對于Goroutine的管理也極為複雜。
4. 棧管理
每個Goroutine都有自己的棧,每個棧大小都不同。如果采用正常方式,以最差情況為每個棧配置設定足夠大的棧空間,會造成記憶體的極大浪費。為了解決這種問題,gcc引入了Split Stacks技術。建立棧時,隻配置設定一塊比較小的記憶體,如果進行某次函數調用導緻棧空間不足時,就會在其他地方配置設定一塊新的棧空間。新的空間不需要和老的棧空間連續。函數調用的參數會拷貝到新的棧空間中,接下來的函數執行都在新棧空間中進行。
4.1 Split Stacks存在問題:
1-
hot split
拆分熱點
如果堆棧幾乎滿了,調用将強制配置設定新的堆棧塊。當該調用傳回時,将釋放新的堆棧塊。如果相同的調用在緊密循環中重複發生,則alloc/free的開銷會導緻很大的開銷。
2-棧的伸縮不能一次完成
棧
allocation/deallocation
工作永無止境,每次堆棧大小在任一方向通過門檻值時,都需要額外的工作。
Goroutine為了解決Splie Stacks的效率問題,使用了連續棧技術(Contiguous stacks)。
- 所有G一開始預設配置設定最小棧,在G結束時回收棧,在新的G開始時,從回收的棧裡擷取一個。即棧是可以重複利用的,減少了記憶體的申請釋放。
- 并且Go會記錄每個goroutine的棧大小,在下次配置設定時預先配置設定足夠大的棧。
- go沒有每次都計算新的棧大小,隻是簡單的以指數級方式增長棧大小,一次不行則重複,直到棧大小足夠。
5. goroutine搶占
Goroutine排程不像OS線程排程那樣有時間片的概念,是以實際搶占機制要弱很多:Go中的搶占實際上是為G設定搶占标記(g.stackguard0=stackPreempt),在後續可以觸發排程的時候進行判斷。
1. 排程器組成
排程器采用MPG,以及schedt來儲存排程器所有狀态資訊。
go排程器主要有4個重要結構:
m
,
p
,
g
,
schedt
。
定義在
src\runtime\runtime2.go
中。
1.1 M:代表作業系統線程
一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,裡面維護小對象記憶體cache(mcache)、目前執行的goroutine、随機數發生器等資訊。
m的最大個數為p的個數。
M的狀态:
- spinning bool; M處于自旋中,M目前沒有執行代碼,擁有P,并等待擷取一個G。
1.2 P: M處理G時的上下文。
Processor,它也維護了一個goroutine隊列,存儲所有需要它來執行的goroutine。一個P最多256個goroutine。
P的數量預設為系統CPU個數。由環境變量
GOMAXPROCS
,或者
runtime.GOMAXPROCS()
設定。
P與M的關系不固定,當P空閑時,就與M解綁。
有兩個設定p個數的觸發場景:
- 在程序剛啟動時,由
調用schedinit()
建立所有的p。procresize()
- 運作時,通過
調用runtime.GOMAXPROCS()
。procresize()
P的狀态:
-
:當M發現無待運作的G時會進入休眠, 這時M擁有的P會變為空閑并加到空閑P連結清單中。_Pidle
-
:當M擁有了一個P後, 這個P的狀态就會變為運作中, M運作G會使用這個P中的資源_Prunning
-
:進入系統調用時,go運作時在系統調用前插代碼_Psyscall
中設定。reentersyscall()
-
:當gc停止了整個世界(STW)時, P會變為此狀态。_Pgcstop
-
:當P的數量在運作時改變(調用_Pdead
, 且數量減少時多餘的P會變為此狀态。procresize()
1.3 G:實作的核心結構
goroutine,G維護了goroutine需要的棧、程式計數器以及它所在的M等資訊。
G一般配置設定給目前的P,目前P的可執行隊列滿了以後,G被放入全局隊列,同時将本地隊列一半移到全局隊列。
當P的隊列空了的時候,會從全局隊列偷取一批(最多32個)到本地隊列。
goroutine狀态:
- _Gidle=0, 剛建立的G,還未初始化。
- _Grunnable=1,處于run隊列中(本地、全局),還沒擁有棧。
- _Grunning=2,可以執行代碼了,G擁有棧,不在run隊列中,并且被配置設定給了M,P。
- _Gsyscall=3,G正在執行一個系統調用,沒有執行使用者代碼,擁有棧,不在run隊列,被配置設定了M。
- _Gwaiting=4,G被runtime阻塞,沒有執行使用者代碼,沒有在run隊列,不擁有棧。
- _Gmoribund_unused=5,該狀态未使用。
- _Gdead=6,G未使用,有三種情況處于該狀态:1.剛退出。2.處于free清單。3.剛把初始化。沒有執行使用者代碼。
- _Genqueue_unused=7,該狀态未使用。
- _Gcopystack=8,G的棧正在被移動,沒有執行使用者代碼。
-
_Gscan=0x1000,
_Gscanrunnable = _Gscan + _Grunnable // 0x1001
_Gscanrunning = _Gscan + _Grunning // 0x1002
_Gscansyscall = _Gscan + _Gsyscall // 0x1003
_Gscanwaiting = _Gscan + _Gwaiting // 0x1004
2. MPG關系
2.1. G與P
G放在P上才可以被運作。有三種方式擷取一個G。
- 本地隊列
,通過P.runq
擷取。runqget()
- 全局隊列
,通過sched.runq
擷取。globrunqget()
- 從其他P上偷取,通過
擷取。runqsteal()
當一個G被建立出來,或者變為可執行狀态時,就把他放到P的可執行隊列中。當一個G執行結束時,P會從隊列中把該G取出;
通過P,避免了每次都從全局隊列中擷取G,避免了全局鎖。
2.2. P與M
P要與M關聯才可以運作。通過
acquirep()
将P與M關聯。通過
releasep()
将P與M解綁。
3. 需要綁定在M上才能運作;
2.3 m0,g0
go中有特殊的M和G, 它們是m0和g0。m0負責執行初始化操作和啟動第一個G。g0是僅用于負責排程的G, g0不指向任何可執行的函數, 每個m都會有一個自己的g0。
var (
m0 m
g0 g
raceprocctx0 uintptr
)
3. m,p,g結構定義
3.1. m
主要成員如下:
-
,排程和執行系統調用都會切換到g0棧去執行。g0 *g
-
,目前正在執行的goroutinecurg *g
-
,目前m附着的p,用來執行goroutine。p puintptr
-
,執行系統調用前附着的p。oldp puintptr
-
: 喚醒M時, M會擁有這個Pnextp puintptr
-
,線程喚醒用的key。park note
-
,辨別M處于自旋狀态spinning bool
3.2 p
主要成員:
-
,p狀态status uint32
-
,附着在的mm muintptr
-
,待執行g對象清單,最多256個。runq [256]guintptr
-
,可複用g對象清單。gFree
-
, 下一個P.link puintptr
-
,背景GC的worker函數, 如果它存在M會優先執行它gcBgMarkWorker
-
,gcw gcWork
3.3. g
主要成員:
-
,棧空間stack
-
,g對象的排程資料,當goroutine被中斷時,目前的狀态資料儲存在這裡。sched gobuf
-
,g的狀态atomicstatus uint32
-
,搶占信号preempt bool
4. 排程器schedule()首次調用
go程序啟動後,
- 會先進行排程器的初始化schedinit()(見12-go程序啟動過程)。
- 然後調用
建立一個runtime·newproc
,用于執行goroutine
。runtime.main
- 然後調用
首次調用runtime·mstart
,執行schedule()
goroutine.runtime.main
-
中建立一個線程,執行runtime.main
,sysmon
會通過sysmon
擷取網絡事件。netpoll
-
執行runtime.main
.main.main
5. 排程器排程時機
goroutine的排程通過
schedule()
進行,觸發
schedule()
調用的時機有:
-
go程式主動觸發
代碼中主動調用
。runtime.Gosched()
-
系統調用
代碼中發起系統調用時,go會在系統調用前後插入代碼。
前插代碼:主要設定目前p進入
狀态,喚醒_Psyscall
sysmon
,觸發gc。将目前p與m解綁。
後插代碼:主要将之前的p與m關聯(如果p還沒被别的m關聯),關聯後直接執行。如果之前的P已經被别的m關聯,則擷取一個空閑的P,擷取失敗,則調用排程器
。schedule()
-
sysmon對p搶占
sysmon通過
來發起對p或者g的搶占,這裡隻是打上标記,真正的搶占在發生函數調用時,通過retake()
進行:newstack()
-
對G搶占
P在同一個G上運作時間超過10MS。就标記G為需要搶占,
和g.preempt=true
。g.stackguard0 = stackPreempt
-
對P搶占
P處于系統調用狀态,判斷是否需要發起搶占。當超過1個sysmon排程周期(至少20us),當P上有待執行任務,或者發起系統調用時間超過10MS,就會對P發起搶占(
)。handoffp()
主要功能就是判斷是否需要将P交給另外一個m執行。P上有待執行任務,或者全局隊列上有待執行任務,就将P交個另外一個m執行。如果P沒有任務可執行,就将P放入空閑清單。handoffp()
-
-
sysmon對異步事件處理
sysmon會每10MS進行一次netpoll操作,有事件觸發時,調用
将G放入全局待執行清單,如果此時有P處于空閑狀态,則針對每個P執行一次injectglist()
擷取一個空閑的P,如果有空閑的M則喚醒,沒有則建立一個M執行P。startm(nil,false)
-
select觸發gopark()
當對chan進行select操作時,如果chan上沒有資料,則會調用
,gopark()
中,如果G允許被搶占,則設定搶占标記,并調用gopark()
将G與M解除綁定,然後觸發排程器調用,park_m()
。schedule()
6. 排程器實作schedule()
- 擷取目前的G。
- 判斷是否處于GC中,是則循環等待GC結束。
- 調用
,判斷是否可以進行目前p上的gc。findRunnableGCWorker()
- 沒有可以進行的gc,則調用
,每60次schedule()調用,就優先從全局隊列中取可執行的G;防止全局清單中的G得不到執行。globrunqget()
- 調用
,擷取一個可以運作的G。此過程是阻塞的,直到擷取到一個可以執行的G。findrunnable()
- 調用
,執行G。execute()
-
調用execute()
(gogo()
),src\syscall\asm_linux_amd64.s
從sched結構恢複G上次被排程器暫停時的寄存器現場(SP、PC等),然後繼續執行。gogo()
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
// 1. 擷取目前的g。
_g_ := getg()
top:
// 2. 判斷是否處于gc等待狀态。處于等待狀态,則通知m線程sleep,并循環判斷等待直到gc結束。
if sched.gcwaiting != 0 {
gcstopm()
goto top
}
var gp *g
var inheritTime bool
// 3. 查找traceReader goroutine。找到則設定狀态為```_Gwaiting```->```_Grunnable```。
if trace.enabled || trace.shutdown {
gp = traceReader()
if gp != nil {
casgstatus(gp, _Gwaiting, _Grunnable)
traceGoUnpark(gp, 0)
}
}
// 4.沒有traceReader,則查找目前p的背景mark goroutine。
if gp == nil && gcBlackenEnabled != 0 {
gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
}
// 5. 沒有traceReader,也沒有gc背景mark goroutine,則從全局待執行隊列擷取一個g。
// 優先以60%的機率從全局隊列擷取。
if gp == nil {
// Check the global runnable queue once in a while to ensure fairness.
// Otherwise two goroutines can completely occupy the local runqueue
// by constantly respawning each other.
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
// 6. 沒有從全局隊列擷取到g,則從本地隊列擷取。
if gp == nil {
gp, inheritTime = runqget(_g_.m.p.ptr())
...
}
// 7. 調用findrunnable(),擷取一個可以運作的G。次過程是阻塞的,直到擷取到一個可以執行的G。
if gp == nil {
gp, inheritTime = findrunnable() // blocks until work is available
}
// This thread is going to run a goroutine and is not spinning anymore,
// so if it was marked as spinning we need to reset it now and potentially
// start a new spinning M.
if _g_.m.spinning {
resetspinning()
}
if sched.disable.user && !schedEnabled(gp) {
// Scheduling of this goroutine is disabled. Put it on
// the list of pending runnable goroutines for when we
// re-enable user scheduling and look again.
lock(&sched.lock)
if schedEnabled(gp) {
// Something re-enabled scheduling while we
// were acquiring the lock.
unlock(&sched.lock)
} else {
sched.disable.runnable.pushBack(gp)
sched.disable.n++
unlock(&sched.lock)
goto top
}
}
if gp.lockedm != 0 {
// Hands off own p to the locked m,
// then blocks waiting for a new p.
startlockedm(gp)
goto top
}
// 8. 執行G。
execute(gp, inheritTime)
}
6.2. findrunnable()
findrunnable()邏輯比較複雜。主要分為兩個階段top/stop。這裡會一直阻塞直到找到可以運作的G。
6.2.1 top階段
- 循環等待gc結束。
- 調用
,從目前P的隊列中取G,找到就傳回。runqget()
- 調用
,從全局隊列中取可執行的G,找到就傳回。globrunqget()
- 調用
netpoll(非阻塞調用)
,判斷是否有異步調用結束的G,找到就傳回。
如果發生了異步事件,則調用injectglist(),間接startm()調用,喚醒空閑M,沒有空閑的就建立一個新的M線程。
- 調用
,從其他P的隊列中偷取。runqsteal()
6.2.2 stop階段
如果top階段沒有擷取到可以執行的G,則嘗試執行GC,如果不需要GC,則嘗試從全局隊列、異步事件擷取G。
- 如果處于垃圾回收标記階段,就進行垃圾回收的标記工作,傳回該G。
- 調用
globrunqget()
,從全局隊列中取可執行的G,找到就傳回。
此時如果依然沒有找到可用的G,則說明目前P處于空閑狀态,将其與M解綁,放入空閑清單。
- 調用
netpoll(阻塞調用)
,擷取異步調用結束的G。
由第二步知道,此時目前P已經與M解綁,需要重新找到一個空閑的P,找到就傳回第一個G,如果沒有找到,則通知目前M sleep。并重回top階段。
這裡如果有異步事件發生,則調用injectglist(),間接startm()調用,擷取一個空閑的M喚醒,如果沒有空閑的M,則建立一個M線程。
// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from global queue, poll network.
func findrunnable() (gp *g, inheritTime bool) {
_g_ := getg()
top:
_p_ := _g_.m.p.ptr()
// 1. 持續等待gc結束。
if sched.gcwaiting != 0 {
gcstopm()
goto top
}
// 2. 嘗試從本地等待隊列擷取一個g。找到則傳回該g。
if gp, inheritTime := runqget(_p_); gp != nil {
return gp, inheritTime
}
// 3. 嘗試從全局隊列擷取一個g。全局需要加鎖。找到就傳回該g。
if sched.runqsize != 0 {
lock(&sched.lock)
gp := globrunqget(_p_, 0)
unlock(&sched.lock)
if gp != nil {
return gp, false
}
}
// 4. 對網絡進行poll,判斷是否有網絡事件發生。如果有則設定g狀态```_Gwaiting```->```_Grunnable```。傳回第一個g。
// Poll network.
// This netpoll is only an optimization before we resort to stealing.
// We can safely skip it if there are no waiters or a thread is blocked
// in netpoll already. If there is any kind of logical race with that
// blocked thread (e.g. it has already returned from netpoll, but does
// not set lastpoll yet), this thread will do blocking netpoll below
// anyway.
if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
if list := netpoll(false); !list.empty() { // non-blocking
gp := list.pop()
injectglist(&list)
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.enabled {
traceGoUnpark(gp, 0)
}
return gp, false
}
}
// 5. 沒有網絡事件發生,則從其他p偷取一個g。如果此時空閑的p達到```GOMAXPROCS-1```,進入stop階段。
// Steal work from other P's.
procs := uint32(gomaxprocs)
if atomic.Load(&sched.npidle) == procs-1 {
// Either GOMAXPROCS=1 or everybody, except for us, is idle already.
// New work can appear from returning syscall/cgocall, network or timers.
// Neither of that submits to local run queues, so no point in stealing.
goto stop
}
// 6. 如果spinning狀态的M數量 >= 繁忙狀态P數量,就阻塞,避免CPU過于繁忙。
// If number of spinning M's >= number of busy P's, block.
// This is necessary to prevent excessive CPU consumption
// when GOMAXPROCS>>1 but the program parallelism is low.
if !_g_.m.spinning && 2*atomic.Load(&sched.nmspinning) >= procs-atomic.Load(&sched.npidle) {
goto stop
}
// 7. 如果m不是出于```spinning```狀态,則設定為出于```spinning```,并将排程器出于spinning計數器加1。
if !_g_.m.spinning {
_g_.m.spinning = true
atomic.Xadd(&sched.nmspinning, 1)
}
// 8. 随機從其他p中偷取一個g,會嘗試4次。偷到則傳回。
for i := 0; i < 4; i++ {
for enum := stealOrder.start(fastrand()); !enum.done(); enum.next() {
if sched.gcwaiting != 0 {
goto top
}
stealRunNextG := i > 2 // first look for ready queues with more than 1 g
if gp := runqsteal(_p_, allp[enum.position()], stealRunNextG); gp != nil {
return gp, false
}
}
}
stop:
// 9. 到目前為止還沒找到g,判斷是否可以進行gc掃描。
// 如果p上的gc掃描work可以繼續工作,則設定該gc g狀态```_Gwaiting```->```_Grunnable```。傳回該g。
// We have nothing to do. If we're in the GC mark phase, can
// safely scan and blacken objects, and have work to do, run
// idle-time marking rather than give up the P.
if gcBlackenEnabled != 0 && _p_.gcBgMarkWorker != 0 && gcMarkWorkAvailable(_p_) {
_p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
gp := _p_.gcBgMarkWorker.ptr()
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.enabled {
traceGoUnpark(gp, 0)
}
return gp, false
}
// wasm only:
// If a callback returned and no other goroutine is awake,
// then pause execution until a callback was triggered.
if beforeIdle() {
// At least one goroutine got woken.
goto top
}
// Before we drop our P, make a snapshot of the allp slice,
// which can change underfoot once we no longer block
// safe-points. We don't need to snapshot the contents because
// everything up to cap(allp) is immutable.
allpSnapshot := allp
// return P and block
lock(&sched.lock)
if sched.gcwaiting != 0 || _p_.runSafePointFn != 0 {
unlock(&sched.lock)
goto top
}
// 10. 再次嘗試從全局隊列擷取g,如果全局可運作隊列不為空,則從全局隊列擷取一個g。
if sched.runqsize != 0 {
gp := globrunqget(_p_, 0)
unlock(&sched.lock)
return gp, false
}
// 11. 全局隊列為空,說明目前p處于空閑狀态,則将p與m解綁。
if releasep() != _p_ {
throw("findrunnable: wrong p")
}
pidleput(_p_)
unlock(&sched.lock)
// Delicate dance: thread transitions from spinning to non-spinning state,
// potentially concurrently with submission of new goroutines. We must
// drop nmspinning first and then check all per-P queues again (with
// #StoreLoad memory barrier in between). If we do it the other way around,
// another thread can submit a goroutine after we've checked all run queues
// but before we drop nmspinning; as the result nobody will unpark a thread
// to run the goroutine.
// If we discover new work below, we need to restore m.spinning as a signal
// for resetspinning to unpark a new worker thread (because there can be more
// than one starving goroutine). However, if after discovering new work
// we also observe no idle Ps, it is OK to just park the current thread:
// the system is fully loaded so no spinning threads are required.
// Also see "Worker thread parking/unparking" comment at the top of the file.
wasSpinning := _g_.m.spinning
if _g_.m.spinning {
_g_.m.spinning = false
if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {
throw("findrunnable: negative nmspinning")
}
}
// 12. 再次檢查全局p清單,找到待運作隊列不為空的P,同時找到一個空閑的P,将其與目前M關聯。回到top階段嘗試讓其執行該G。
// check all runqueues once again
for _, _p_ := range allpSnapshot {
if !runqempty(_p_) {
lock(&sched.lock)
_p_ = pidleget()
unlock(&sched.lock)
if _p_ != nil {
acquirep(_p_)
if wasSpinning {
_g_.m.spinning = true
atomic.Xadd(&sched.nmspinning, 1)
}
goto top
}
break
}
}
// 13. 沒有找到可以執行的G,則再次嘗試進行GC。
// Check for idle-priority GC work again.
if gcBlackenEnabled != 0 && gcMarkWorkAvailable(nil) {
lock(&sched.lock)
_p_ = pidleget()
if _p_ != nil && _p_.gcBgMarkWorker == 0 {
pidleput(_p_)
_p_ = nil
}
unlock(&sched.lock)
if _p_ != nil {
acquirep(_p_)
if wasSpinning {
_g_.m.spinning = true
atomic.Xadd(&sched.nmspinning, 1)
}
// Go back to idle GC check.
goto stop
}
}
// 14. 阻塞調用netpoll()。如果有事件發生,并且擷取到一個空閑的P,将其與目前M關聯,傳回第一個G。
// poll network
if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Xchg64(&sched.lastpoll, 0) != 0 {
if _g_.m.p != 0 {
throw("findrunnable: netpoll with p")
}
if _g_.m.spinning {
throw("findrunnable: netpoll with spinning")
}
list := netpoll(true) // block until new work is available
atomic.Store64(&sched.lastpoll, uint64(nanotime()))
if !list.empty() {
lock(&sched.lock)
_p_ = pidleget()
unlock(&sched.lock)
if _p_ != nil {
acquirep(_p_)
gp := list.pop()
// 15. 設定可執行G狀态```_Gwaiting```->````_Grunnable```。都放入全局待執行隊列。
// 根據空閑P個數,調用startm()
injectglist(&list)
casgstatus(gp, _Gwaiting, _Grunnable)
if trace.enabled {
traceGoUnpark(gp, 0)
}
return gp, false
}
injectglist(&list)
}
}
// 16. 将m與p解綁,通知m線程sleep。
stopm()
// 17. 繼續循環,找到可以執行的G。
goto top
}
7. gcstopm()
- 将p與m解綁。
- 設定p的狀态為
。_Pgcstop
- 通知m線程sleep。
// Stops the current m for stopTheWorld.
// Returns when the world is restarted.
func gcstopm() {
_g_ := getg()
if sched.gcwaiting == 0 {
throw("gcstopm: not waiting for gc")
}
if _g_.m.spinning {
_g_.m.spinning = false
// OK to just drop nmspinning here,
// startTheWorld will unpark threads as necessary.
if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {
throw("gcstopm: negative nmspinning")
}
}
// 将p與m解綁
_p_ := releasep()
lock(&sched.lock)
// 設定p的狀态為_Pgcstop.
_p_.status = _Pgcstop
sched.stopwait--
if sched.stopwait == 0 {
notewakeup(&sched.stopnote)
}
unlock(&sched.lock)
// 通知m線程sleep。
stopm()
}
stopm()
- 将排程器目前正在等待工作指針設定為目前m。
。給等待計數器加1.sched.midle = m
.sched.nmidle++
- 通知m線程sleep。
- 将m.nextp與m關聯。同時置m的nextp為0.
// Stops execution of the current m until new work is available.
// Returns with acquired P.
func stopm() {
_g_ := getg()
if _g_.m.locks != 0 {
throw("stopm holding locks")
}
if _g_.m.p != 0 {
throw("stopm holding p")
}
if _g_.m.spinning {
throw("stopm spinning")
}
lock(&sched.lock)
mput(_g_.m)
unlock(&sched.lock)
notesleep(&_g_.m.park)
noteclear(&_g_.m.park)
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}