golang源碼分析:調度器chan調度

golang調度機制chan調度

golang的調度策略中,碰見阻塞chan就會將該chan放入到阻塞的g中,然後再等待該chan被喚醒,這是golang調度器策略的主動調度策略之一,其中還有其他的主動調度策略包括進入調用系統調用或者主動調用Gosched時,都會發生主動調度,對應的當然也有被動調度,被動調度主要是後臺線程在檢查到某一個g執行的時間過長,再該g進行棧擴展(即調用函數時)就會進行被動調度,這些內容後續再討論。本文先了解一下golang在阻塞chan下的調度機制。

chan阻塞調度示例代碼

package main

import "fmt"

func main(){
   chan_w := make(chan int)
   go func(){
      chan_w <- 1
      fmt.Println("over")
   }()
   <- chan_w
   fmt.Println("main over")
}

示例代碼相對簡單,即創建一個chan_w,調用一個協程去傳入值,然後主協程就等待chan_w的結果返回,此時我們查看一下對應的彙編輸出。

# go build main.go
# go tool objdump -s "main\.main" main
TEXT main.main(SB) /root/open_falcon/main.go
  main.go:5		0x4871b0		64488b0c25f8ffffff	MOVQ FS:0xfffffff8, CX			
  main.go:5		0x4871b9		483b6110		CMPQ 0x10(CX), SP			
  main.go:5		0x4871bd		0f86c3000000		JBE 0x487286				
  main.go:5		0x4871c3		4883ec60		SUBQ $0x60, SP				
  main.go:5		0x4871c7		48896c2458		MOVQ BP, 0x58(SP)			
  main.go:5		0x4871cc		488d6c2458		LEAQ 0x58(SP), BP			
  main.go:6		0x4871d1		488d05c8050100		LEAQ 0x105c8(IP), AX			
  main.go:6		0x4871d8		48890424		MOVQ AX, 0(SP)				
  main.go:6		0x4871dc		48c744240800000000	MOVQ $0x0, 0x8(SP)			
  main.go:6		0x4871e5		e886d2f7ff		CALL runtime.makechan(SB)		
  main.go:6		0x4871ea		488b442410		MOVQ 0x10(SP), AX			
  main.go:6		0x4871ef		4889442440		MOVQ AX, 0x40(SP)			
  main.go:7		0x4871f4		c7042408000000		MOVL $0x8, 0(SP)			
  main.go:7		0x4871fb		488d0d268c0300		LEAQ 0x38c26(IP), CX			
  main.go:7		0x487202		48894c2408		MOVQ CX, 0x8(SP)			
  main.go:7		0x487207		e8e49efaff		CALL runtime.newproc(SB)		
  main.go:11		0x48720c		488b442440		MOVQ 0x40(SP), AX			
  main.go:11		0x487211		48890424		MOVQ AX, 0(SP)				
  main.go:11		0x487215		48c744240800000000	MOVQ $0x0, 0x8(SP)			
  main.go:11		0x48721e		e8bddff7ff		CALL runtime.chanrecv1(SB)		
  main.go:12		0x487223		0f57c0			XORPS X0, X0				
  main.go:12		0x487226		0f11442448		MOVUPS X0, 0x48(SP)			
  main.go:12		0x48722b		488d05ae110100		LEAQ 0x111ae(IP), AX			
  main.go:12		0x487232		4889442448		MOVQ AX, 0x48(SP)			
  main.go:12		0x487237		488d05b2850400		LEAQ main.statictmp_0(SB), AX		
  main.go:12		0x48723e		4889442450		MOVQ AX, 0x50(SP)			
  main.go:12		0x487243		90			NOPL					
  print.go:275		0x487244		488b05a5050d00		MOVQ os.Stdout(SB), AX			
  print.go:275		0x48724b		488d0dce9a0400		LEAQ go.itab.*os.File,io.Writer(SB), CX	
  print.go:275		0x487252		48890c24		MOVQ CX, 0(SP)				
  print.go:275		0x487256		4889442408		MOVQ AX, 0x8(SP)			
  print.go:275		0x48725b		488d442448		LEAQ 0x48(SP), AX			
  print.go:275		0x487260		4889442410		MOVQ AX, 0x10(SP)			
  print.go:275		0x487265		48c744241801000000	MOVQ $0x1, 0x18(SP)			
  print.go:275		0x48726e		48c744242001000000	MOVQ $0x1, 0x20(SP)			
  print.go:275		0x487277		e8e498ffff		CALL fmt.Fprintln(SB)			
  print.go:275		0x48727c		488b6c2458		MOVQ 0x58(SP), BP			
  print.go:275		0x487281		4883c460		ADDQ $0x60, SP				
  print.go:275		0x487285		c3			RET					
  main.go:5		0x487286		e8b580fcff		CALL runtime.morestack_noctxt(SB)	
  main.go:5		0x48728b		e920ffffff		JMP main.main(SB)			

TEXT main.main.func1(SB) /root/open_falcon/main.go
  main.go:7		0x487290		64488b0c25f8ffffff	MOVQ FS:0xfffffff8, CX			
  main.go:7		0x487299		483b6110		CMPQ 0x10(CX), SP			
  main.go:7		0x48729d		0f868b000000		JBE 0x48732e				
  main.go:7		0x4872a3		4883ec58		SUBQ $0x58, SP				
  main.go:7		0x4872a7		48896c2450		MOVQ BP, 0x50(SP)			
  main.go:7		0x4872ac		488d6c2450		LEAQ 0x50(SP), BP			
  main.go:8		0x4872b1		488b442460		MOVQ 0x60(SP), AX			
  main.go:8		0x4872b6		48890424		MOVQ AX, 0(SP)				
  main.go:8		0x4872ba		488d05ff810400		LEAQ main.statictmp_1(SB), AX		
  main.go:8		0x4872c1		4889442408		MOVQ AX, 0x8(SP)			
  main.go:8		0x4872c6		e8d5d3f7ff		CALL runtime.chansend1(SB)		
  main.go:9		0x4872cb		0f57c0			XORPS X0, X0				
  main.go:9		0x4872ce		0f11442440		MOVUPS X0, 0x40(SP)			
  main.go:9		0x4872d3		488d0506110100		LEAQ 0x11106(IP), AX			
  main.go:9		0x4872da		4889442440		MOVQ AX, 0x40(SP)			
  main.go:9		0x4872df		488d051a850400		LEAQ main.statictmp_2(SB), AX		
  main.go:9		0x4872e6		4889442448		MOVQ AX, 0x48(SP)			
  main.go:9		0x4872eb		90			NOPL					
  print.go:275		0x4872ec		488b05fd040d00		MOVQ os.Stdout(SB), AX			
  print.go:275		0x4872f3		488d0d269a0400		LEAQ go.itab.*os.File,io.Writer(SB), CX	
  print.go:275		0x4872fa		48890c24		MOVQ CX, 0(SP)				
  print.go:275		0x4872fe		4889442408		MOVQ AX, 0x8(SP)			
  print.go:275		0x487303		488d442440		LEAQ 0x40(SP), AX			
  print.go:275		0x487308		4889442410		MOVQ AX, 0x10(SP)			
  print.go:275		0x48730d		48c744241801000000	MOVQ $0x1, 0x18(SP)			
  print.go:275		0x487316		48c744242001000000	MOVQ $0x1, 0x20(SP)			
  print.go:275		0x48731f		e83c98ffff		CALL fmt.Fprintln(SB)			
  print.go:275		0x487324		488b6c2450		MOVQ 0x50(SP), BP			
  print.go:275		0x487329		4883c458		ADDQ $0x58, SP				
  print.go:275		0x48732d		c3			RET					
  main.go:7		0x48732e		e80d80fcff		CALL runtime.morestack_noctxt(SB)	
  main.go:7		0x487333		e958ffffff		JMP main.main.func1(SB)	

從彙編信息中可以看出,首先調用了runtime的makechan方法,接着就調用了runtime.newproc函數將func包裝成爲一個協程,此時主協程就調用了runtime的chanrecv1方法等待協程的數據返回。在golang的初始化啓動流程中,golang會將main包裝成爲一個主協程進行運行,接着就分析一下具體的執行流程。

chan的主動讓出調度與喚醒

chanrecv1主動選擇調度
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// raceenabled: don't need to check ep, as it is always on the stack
	// or is new memory allocated by reflect.

	if debugChan {  														// 是否是調試模式
		print("chanrecv: chan=", c, "\n")
	}

	if c == nil {                               // 如果傳入的chan爲空
		if !block {                               // 如果是非阻塞的則返回
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)   // 設置等待爲一個接受爲空的chan
		throw("unreachable")
	}

	// Fast path: check for failed non-blocking operation without acquiring the lock.
	//
	// After observing that the channel is not ready for receiving, we observe that the
	// channel is not closed. Each of these observations is a single word-sized read
	// (first c.sendq.first or c.qcount, and second c.closed).
	// Because a channel cannot be reopened, the later observation of the channel
	// being not closed implies that it was also not closed at the moment of the
	// first observation. We behave as if we observed the channel at that moment
	// and report that the receive cannot proceed.
	//
	// The order of operations is important here: reversing the operations can lead to
	// incorrect behavior when racing with a close.
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {    
		return
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)                			// 該chan加鎖

	if c.closed != 0 && c.qcount == 0 {   // 如果該chan不是關閉狀態並且初始緩存個數不爲0
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)                     // 解鎖  返回
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

	if sg := c.sendq.dequeue(); sg != nil {
		// Found a waiting sender. If buffer is size 0, receive value
		// directly from sender. Otherwise, receive from head of queue
		// and add sender's value to the tail of the queue (both map to
		// the same buffer slot because the queue is full).
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)    // 如果可以立即接受 則喚醒調度等待的協程
		return true, true
	}

	if c.qcount > 0 {                                  // 如果是一個帶緩衝區的chan
		// Receive directly from queue
		qp := chanbuf(c, c.recvx)                        // 則直接從隊列中獲取數據並拷貝到chan的接收隊列中
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

	if !block {                         	// 如果非阻塞則直接返回
		unlock(&c.lock)
		return false, false
	}

	// no sender available: block on this channel.
	gp := getg()                          	// 如果沒有可以接受的則阻塞
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false 						
	mysg.c = c
	gp.param = nil
	c.recvq.enqueue(mysg)
	goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)   // 調用其他的協程

	// someone woke us up
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed
}

從chanrecv的執行流程中可以看出,首先先根據chan的類型來判斷是否是可以立即有數據可用,如果有則直接返回執行,如果是帶緩衝區的chan並且chan中也沒有數據可以使用則調用goparkunlock主動調用其他的協程。

goparkunlock函數調度其他協程
func goparkunlock(lock *mutex, reason waitReason, traceEv byte, traceskip int) {
	gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)
}

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
	if reason != waitReasonSleep {
		checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
	}
	mp := acquirem()   						// 獲取m
	gp := mp.curg
	status := readgstatus(gp)
	if status != _Grunning && status != _Gscanrunning {
		throw("gopark: bad g status")
	}
	mp.waitlock = lock
	mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))
	gp.waitreason = reason   					// 等待阻塞的原因
	mp.waittraceev = traceEv
	mp.waittraceskip = traceskip
	releasem(mp)
	// can't do anything that might move the G between Ms here.
	mcall(park_m)                    // 切換棧調用park_m
}

func park_m(gp *g) {
	_g_ := getg()

	if trace.enabled {
		traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
	}

	casgstatus(gp, _Grunning, _Gwaiting)   			// 將該g設置爲阻塞狀態
	dropg() 																		// 接觸g m 的關係

	if _g_.m.waitunlockf != nil {               // waitunlockf是否爲空
		fn := *(*func(*g, unsafe.Pointer) bool)(unsafe.Pointer(&_g_.m.waitunlockf))
		ok := fn(gp, _g_.m.waitlock)             // 如果不爲空則執行該函數
		_g_.m.waitunlockf = nil
		_g_.m.waitlock = nil
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable) 		// 獲取鎖之後就設置該g位可運行狀態
			execute(gp, true) // Schedule it back, never returns.   // 繼續執行該g
		}
	}
	schedule() 																		// 重新調度
}

緊接着就判斷是否需要等待鎖釋放函數,如果沒有需要等待的函數則直接進行schedule進行調度。此時就調度其它協程的執行。

chan的喚醒操作

在main.main.func1彙編代碼中,在傳入值1之後就調用了runtime.chansend1(SB)方法,該方法就是喚醒被該通道阻塞的G

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {                                  // 如果通道爲空
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)  // 向爲空的通道發送數據
		throw("unreachable")
	}

	if debugChan {
		print("chansend: chan=", c, "\n")
	}

	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, funcPC(chansend))
	}

	// Fast path: check for failed non-blocking operation without acquiring the lock.
	//
	// After observing that the channel is not closed, we observe that the channel is
	// not ready for sending. Each of these observations is a single word-sized read
	// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
	// Because a closed channel cannot transition from 'ready for sending' to
	// 'not ready for sending', even if the channel is closed between the two observations,
	// they imply a moment between the two when the channel was both not yet closed
	// and not ready for sending. We behave as if we observed the channel at that moment,
	// and report that the send cannot proceed.
	//
	// It is okay if the reads are reordered here: if we observe that the channel is not
	// ready for sending and then observe that it is not closed, that implies that the
	// channel wasn't closed during the first observation.
	if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
		(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {            
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)                      // 加鎖

	if c.closed != 0 {                
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

	if sg := c.recvq.dequeue(); sg != nil {      // 如果當前接受隊列不爲空
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)   // 向該接受隊列中發送數據
		return true
	}

	if c.qcount < c.dataqsiz {                         // 如果接受的數據爲緩衝區
		// Space is available in the channel buffer. Enqueue the element to send.
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true 																			// 如果緩衝區有值則可以繼續執行
	}

	...
}

其中處理的邏輯也是檢查接受的隊列上面是否有內容,如果有值則直接調用send方法去發送數據,否則則等待chan的鎖的釋放去阻塞等待該值被執行。

// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked.  send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if raceenabled {
		if c.dataqsiz == 0 {
			racesync(c, sg)
		} else {
			// Pretend we go through the buffer, even though
			// we copy directly. Note that we need to increment
			// the head/tail locations only when raceenabled.
			qp := chanbuf(c, c.recvx)
			raceacquire(qp)
			racerelease(qp)
			raceacquireg(sg.g, qp)
			racereleaseg(sg.g, qp)
			c.recvx++
			if c.recvx == c.dataqsiz {
				c.recvx = 0
			}
			c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
		}
	}
	if sg.elem != nil {
		sendDirect(c.elemtype, sg, ep)
		sg.elem = nil
	}
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1)        // 調用goready函數執行
}

func goready(gp *g, traceskip int) {
	systemstack(func() {
		ready(gp, traceskip, true)
	})
}

// Mark gp ready to run.
func ready(gp *g, traceskip int, next bool) {
	if trace.enabled {
		traceGoUnpark(gp, traceskip)         // 是否trace跟蹤
	}

	status := readgstatus(gp)

	// Mark runnable.
	_g_ := getg()
	_g_.m.locks++ // disable preemption because it can be holding p in a local var
	if status&^_Gscan != _Gwaiting {        
		dumpgstatus(gp)
		throw("bad g->status in ready")
	}

	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
	casgstatus(gp, _Gwaiting, _Grunnable)         // 設置狀態爲可運行狀態
	runqput(_g_.m.p.ptr(), gp, next)              // 放入隊列當中
	if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {
		wakep()                        // 如果有空閒的p 且 m沒有處於自旋狀態 則創建一個線程工作
	}
	_g_.m.locks--
	if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in Case we've cleared it in newstack
		_g_.stackguard0 = stackPreempt
	}
}

此時將喚醒的g設置爲可運行狀態並檢查是否有空閒的p,如果有空閒的p並且沒有自旋狀態的線程則調用wakep函數要麼喚醒(喚醒在Linux中主要利用了futex系統調用)一個m或者創建一個新的工作線程。

至此chan對應的阻塞和喚醒的流程基本完成。

總結

本文簡單的分析了一下有關chan的一個阻塞與喚醒的流程,其中主要就是通過設置g的狀態,然後再喚醒等待該chan的協程以便繼續執行,從而完成chan狀態轉移下的調度。由於本人才疏學淺,如有錯誤請批評指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章