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狀態轉移下的調度。由於本人才疏學淺,如有錯誤請批評指正。