天天看點

14-go排程器

引入排程器原因

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)。

  1. 所有G一開始預設配置設定最小棧,在G結束時回收棧,在新的G開始時,從回收的棧裡擷取一個。即棧是可以重複利用的,減少了記憶體的申請釋放。
  2. 并且Go會記錄每個goroutine的棧大小,在下次配置設定時預先配置設定足夠大的棧。
  3. 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的狀态:

  1. 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個數的觸發場景:

  1. 在程序剛啟動時,由

    schedinit()

    調用

    procresize()

    建立所有的p。
  2. 運作時,通過

    runtime.GOMAXPROCS()

    調用

    procresize()

P的狀态:

  1. _Pidle

    :當M發現無待運作的G時會進入休眠, 這時M擁有的P會變為空閑并加到空閑P連結清單中。
  2. _Prunning

    :當M擁有了一個P後, 這個P的狀态就會變為運作中, M運作G會使用這個P中的資源
  3. _Psyscall

    :進入系統調用時,go運作時在系統調用前插代碼

    reentersyscall()

    中設定。
  4. _Pgcstop

    :當gc停止了整個世界(STW)時, P會變為此狀态。
  5. _Pdead

    :當P的數量在運作時改變(調用

    procresize()

    , 且數量減少時多餘的P會變為此狀态。

1.3 G:實作的核心結構

goroutine,G維護了goroutine需要的棧、程式計數器以及它所在的M等資訊。

G一般配置設定給目前的P,目前P的可執行隊列滿了以後,G被放入全局隊列,同時将本地隊列一半移到全局隊列。

當P的隊列空了的時候,會從全局隊列偷取一批(最多32個)到本地隊列。

goroutine狀态:

  1. _Gidle=0, 剛建立的G,還未初始化。
  2. _Grunnable=1,處于run隊列中(本地、全局),還沒擁有棧。
  3. _Grunning=2,可以執行代碼了,G擁有棧,不在run隊列中,并且被配置設定給了M,P。
  4. _Gsyscall=3,G正在執行一個系統調用,沒有執行使用者代碼,擁有棧,不在run隊列,被配置設定了M。
  5. _Gwaiting=4,G被runtime阻塞,沒有執行使用者代碼,沒有在run隊列,不擁有棧。
  6. _Gmoribund_unused=5,該狀态未使用。
  7. _Gdead=6,G未使用,有三種情況處于該狀态:1.剛退出。2.處于free清單。3.剛把初始化。沒有執行使用者代碼。
  8. _Genqueue_unused=7,該狀态未使用。
  9. _Gcopystack=8,G的棧正在被移動,沒有執行使用者代碼。
  10. _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。

  1. 本地隊列

    P.runq

    ,通過

    runqget()

    擷取。
  2. 全局隊列

    sched.runq

    ,通過

    globrunqget()

    擷取。
  3. 從其他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

主要成員如下:

  1. g0 *g

    ,排程和執行系統調用都會切換到g0棧去執行。
  2. curg *g

    ,目前正在執行的goroutine
  3. p puintptr

    ,目前m附着的p,用來執行goroutine。
  4. oldp puintptr

    ,執行系統調用前附着的p。
  5. nextp puintptr

    : 喚醒M時, M會擁有這個P
  6. park note

    ,線程喚醒用的key。
  7. spinning bool

    ,辨別M處于自旋狀态

3.2 p

主要成員:

  1. status uint32

    ,p狀态
  2. m muintptr

    ,附着在的m
  3. runq [256]guintptr

    ,待執行g對象清單,最多256個。
  4. gFree

    ,可複用g對象清單。
  5. link puintptr

    , 下一個P.
  6. gcBgMarkWorker

    ,背景GC的worker函數, 如果它存在M會優先執行它
  7. gcw gcWork

    ,

3.3. g

主要成員:

  1. stack

    ,棧空間
  2. sched gobuf

    ,g對象的排程資料,當goroutine被中斷時,目前的狀态資料儲存在這裡。
  3. atomicstatus uint32

    ,g的狀态
  4. preempt bool

    ,搶占信号

4. 排程器schedule()首次調用

go程序啟動後,

  1. 會先進行排程器的初始化schedinit()(見12-go程序啟動過程)。
  2. 然後調用

    runtime·newproc

    建立一個

    goroutine

    ,用于執行

    runtime.main

  3. 然後調用

    runtime·mstart

    首次調用

    schedule()

    ,執行

    runtime.main

    goroutine.
  4. runtime.main

    中建立一個線程,執行

    sysmon

    sysmon

    會通過

    netpoll

    擷取網絡事件。
  5. runtime.main

    執行

    main.main

    .

5. 排程器排程時機

goroutine的排程通過

schedule()

進行,觸發

schedule()

調用的時機有:

  1. go程式主動觸發

    代碼中主動調用

    runtime.Gosched()

  2. 系統調用

    代碼中發起系統調用時,go會在系統調用前後插入代碼。

    前插代碼:主要設定目前p進入

    _Psyscall

    狀态,喚醒

    sysmon

    ,觸發gc。将目前p與m解綁。

    後插代碼:主要将之前的p與m關聯(如果p還沒被别的m關聯),關聯後直接執行。如果之前的P已經被别的m關聯,則擷取一個空閑的P,擷取失敗,則調用排程器

    schedule()

  3. sysmon對p搶占

    sysmon通過

    retake()

    來發起對p或者g的搶占,這裡隻是打上标記,真正的搶占在發生函數調用時,通過

    newstack()

    進行:
    1. 對G搶占

      P在同一個G上運作時間超過10MS。就标記G為需要搶占,

      g.preempt=true

      g.stackguard0 = stackPreempt

    2. 對P搶占

      P處于系統調用狀态,判斷是否需要發起搶占。當超過1個sysmon排程周期(至少20us),當P上有待執行任務,或者發起系統調用時間超過10MS,就會對P發起搶占(

      handoffp()

      )。

      handoffp()

      主要功能就是判斷是否需要将P交給另外一個m執行。P上有待執行任務,或者全局隊列上有待執行任務,就将P交個另外一個m執行。如果P沒有任務可執行,就将P放入空閑清單。
  4. sysmon對異步事件處理

    sysmon會每10MS進行一次netpoll操作,有事件觸發時,調用

    injectglist()

    将G放入全局待執行清單,如果此時有P處于空閑狀态,則針對每個P執行一次

    startm(nil,false)

    擷取一個空閑的P,如果有空閑的M則喚醒,沒有則建立一個M執行P。
  5. select觸發gopark()

    當對chan進行select操作時,如果chan上沒有資料,則會調用

    gopark()

    gopark()

    中,如果G允許被搶占,則設定搶占标記,并調用

    park_m()

    将G與M解除綁定,然後觸發排程器調用,

    schedule()

6. 排程器實作schedule()

  1. 擷取目前的G。
  2. 判斷是否處于GC中,是則循環等待GC結束。
  3. 調用

    findRunnableGCWorker()

    ,判斷是否可以進行目前p上的gc。
  4. 沒有可以進行的gc,則調用

    globrunqget()

    ,每60次schedule()調用,就優先從全局隊列中取可執行的G;防止全局清單中的G得不到執行。
  5. 調用

    findrunnable()

    ,擷取一個可以運作的G。此過程是阻塞的,直到擷取到一個可以執行的G。
  6. 調用

    execute()

    ,執行G。
  7. execute()

    調用

    gogo()

    (

    src\syscall\asm_linux_amd64.s

    ),

    gogo()

    從sched結構恢複G上次被排程器暫停時的寄存器現場(SP、PC等),然後繼續執行。
// 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階段
  1. 循環等待gc結束。
  2. 調用

    runqget()

    ,從目前P的隊列中取G,找到就傳回。
  3. 調用

    globrunqget()

    ,從全局隊列中取可執行的G,找到就傳回。
  4. 調用

    netpoll(非阻塞調用)

    ,判斷是否有異步調用結束的G,找到就傳回。

    如果發生了異步事件,則調用injectglist(),間接startm()調用,喚醒空閑M,沒有空閑的就建立一個新的M線程。

  5. 調用

    runqsteal()

    ,從其他P的隊列中偷取。

6.2.2 stop階段

如果top階段沒有擷取到可以執行的G,則嘗試執行GC,如果不需要GC,則嘗試從全局隊列、異步事件擷取G。

  1. 如果處于垃圾回收标記階段,就進行垃圾回收的标記工作,傳回該G。
  2. 調用

    globrunqget()

    ,從全局隊列中取可執行的G,找到就傳回。

    此時如果依然沒有找到可用的G,則說明目前P處于空閑狀态,将其與M解綁,放入空閑清單。

  3. 調用

    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()

  1. 将p與m解綁。
  2. 設定p的狀态為

    _Pgcstop

  3. 通知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()

  1. 将排程器目前正在等待工作指針設定為目前m。

    sched.midle = m

    。給等待計數器加1.

    sched.nmidle++

    .
  2. 通知m線程sleep。
  3. 将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
}