這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
主協程初始化
Golang的主協程指的是運行main函數的協程,而子協程指的是在程式運行過程中由主協程建立的協程。每個線程(m)只會有一個主協程,而子協程可能會有很多很多。
子協程和主協程在概念和內部實現上幾乎沒有任何區別,唯一的不同在於它們的初始棧大小不同。
我們先看看測試過程中產生的主協程堆棧樣本。我測試代碼中就產生了一個主協程,通過反組譯碼代碼看到他的樣子大概如下:
主協程啟動
分析連接器(libinit())發現go程式的入口函數是_rt0_amd64_linux(linux amd64機器)
子協程初始化
Golang子協程堆棧在協程被建立時也一併建立,代碼如下:
func newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g { _g_ := getg() ...... _p_ := _g_.m.p.ptr() newg := gfget(_p_) if newg == nil { // 建立協程棧 newg = malg(_StackMin) casgstatus(newg, _Gidle, _Gdead) allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack. } ...... totalSize := 4*regSize + uintptr(siz) // extra space in case of reads slightly beyond frame if hasLinkRegister { totalSize += ptrSize } totalSize += -totalSize & (spAlign - 1) // align to spAlign // 新協程的棧頂計算,將棧的基地址減去參數佔用的空間 sp := newg.stack.hi - totalSize spArg := sp if hasLinkRegister { // caller's LR *(*unsafe.Pointer)(unsafe.Pointer(sp)) = nil spArg += ptrSize } ... // 設定建立協程的棧頂sp newg.sched.sp = sp}// Allocate a new g, with a stack big enough for stacksize bytes.func malg(stacksize int32) *g { newg := new(g) if stacksize >= 0 { stacksize = round2(_StackSystem + stacksize) systemstack(func() { newg.stack, newg.stkbar = stackalloc(uint32(stacksize)) }) // 設定stackguard,在協程棧不夠用時再重新申請新的棧 newg.stackguard0 = newg.stack.lo + _StackGuard newg.stackguard1 = ^uintptr(0) newg.stackAlloc = uintptr(stacksize) } return newg}
在go1.5.1版本中,_StackMin大小被定義為2048(而在1.3.2版本中該值還是8192),也即每個協程的初始堆棧大小為2KB,相當小了。縮小該值的好處是即使建立了很多的協程也不會導致記憶體使用量的急劇增長。
另外,在協程棧空間被分配出來後,還需要作一些其他的初始化,主要是協程棧頂的設定以及堆棧保護的設定。
以下是我對源碼加入一些調試資訊後列印的協程建立堆棧的詳細資料:
create goroutine, stack:{stackLow: 859530534912, stackHi:859530536928, sp:859530536880, stackguard:859530535552}
根據上面的分析以及列印資訊可以大致勾勒出協程初始化狀態的堆棧:
https://tracymacding.gitbooks.io/implementation-of-golang/stack/stack_overview.html https://tracymacding.gitbooks.io/implementation-of-golang/stack/stack_enlarge.html