1. 程式人生 > 程式設計 >golang核心原理-協程棧

golang核心原理-協程棧

什麼是協程棧

每個協程都需要有自己的棧空間,來存放變數,函式,暫存器等資訊。所以系統需要給協程分配足夠的棧空間。

棧分配方式

固定大小的棧

每個協程都有相同的,固定大小的棧。

優點:實現簡單;

缺點:每個協程需要的棧空間不盡相同,如果一概而論,那麼有些是浪費,有些是不夠用。

建立時指定

由開發者在建立時指定協程棧大小。java,c++在建立執行緒時可以指定其棧大小。

優點:實現簡單

缺點:對開發者要求比較高,需要根據棧變數,請求量預估。但是有些場景不太好預估,比如遞迴呼叫,這種情況通常只能往大的估計。

Segmented stacks

分配和釋法額外的記憶體空間。初始分配的比較小的空間,如4k。不夠了再增加,用完即釋放。以下是一個例子:

當G呼叫H的時候,沒有足夠的棧空間來讓H執行,這時候Go執行環境就會從堆裡分配一個新的棧記憶體塊去讓H執行。在H返回到G之前,新分配的記憶體塊被釋放回堆。這種管理棧的方法一般都工作得很好。但對有些程式碼,特別是遞迴呼叫,它會造成程式不停地分配和釋放新的記憶體空間。舉個例子,在一個程式裡,函式G會在一個迴圈裡呼叫很多次H函式。每次呼叫都會分配一塊新的記憶體空間。這就是熱分裂問題(hot split problem)。

優點:動態擴充套件,初始成本小,可以將協程當作廉價資源使用。

缺點:存在熱分裂問題(hot split problem)。

Stack copying

動態擴充套件,分配更大的記憶體,做指標遷移。

優點:動態擴充套件,初始成本小,可以將協程當作廉價資源使用,且不存在hot split problem問題

缺點:由於通常以2倍擴充套件,當請求量密集,記憶體敏感的情況下,記憶體會消耗比較多,容易oom,當然,通常的業務量是ok的,不會有任何問題。同時100w連線才要考慮優化。

golang 棧分配方式

1.3之前採用的是Segmented stacks的方式。之後採用的Stack copying,也叫continuous stack(連續棧)

棧擴容

觸發時機

執行時,發現棧不夠用了

關鍵步驟

  1. 將狀態從 _Grunning 更新至 _Gcopystack
  2. 計算出需要申請的資料大小
  3. copystack,進行棧複製,後面會詳細分析
  4. 將協程狀態恢復至_Grunning
  5. 走一遍協程排程

關鍵原始碼

func newstack() {
    thisg := getg()
    ......
    gp := thisg.m.curg
    ......
    // Allocate a bigger segment and move the stack.
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize * 2 // 比原來大一倍
    ......
    // The goroutine must be executing in order to call newstack,// so it must be Grunning (or Gscanrunning).
    casgstatus(gp,_Grunning,_Gcopystack) //修改協程狀態

    // The concurrent GC will not scan the stack while we are doing 
    // the copy since the gp is in a Gcopystack status.
    copystack(gp,newsize,true) //在下面會講到
    ......
    casgstatus(gp,_Gcopystack,_Grunning)
    gogo(&gp.sched)
}
複製程式碼

棧縮容

觸發時機

gc進行時,非執行中協程,棧使用不超過1/4的,會縮容為原來1/2

關鍵步驟

  1. 檢查協程狀態,如果已經結束,則釋放空間
  2. 確定新空間size,目前為原來1/2
  3. 檢查棧使用是否超過1/4,若沒有,則放棄
  4. copystack,進行棧複製,後面會詳細分析

關鍵原始碼

func shrinkstack(gp *g) {
    gstatus := readgstatus(gp)
    if gstatus&^_Gscan == _Gdead {
	    if gp.stack.lo != 0 {
	        // Free whole stack - it will get reallocated
	        // if G is used again.
	        stackfree(gp.stack)
	        gp.stack.lo = 0
	        gp.stack.hi = 0
	    }
	    return
    }
    ......
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize / 2 // 比原來小1倍
    if newsize < _FixedStack {
        return
    }
    // Compute how much of the stack is currently in use and only
    // shrink the stack if gp is using less than a quarter of its
    // current stack. The currently used stack includes everything
    // down to the SP plus the stack guard space that ensures
    // there's room for nosplit functions.
    avail := gp.stack.hi - gp.stack.lo
    //當已使用的棧佔不到總棧的1/4 進行縮容
    if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
        return
    }

    copystack(gp,false) //在下面會講到
}
複製程式碼

copystack棧拷貝過程

原來內容上的拷貝

關鍵步驟

  1. 申請新的棧空間:new := stackalloc(uint32(newsize));
  2. 調整指標指向,將sudog,ctx等,指向新位置,計算方式為原地址+delta(delta為new.hi-old.hi);
  3. gentraceback,調整棧幀到新位置;
  4. memmove老棧資料到新棧;
  5. 刪除老棧。
func copystack(gp *g,newsize uintptr,sync bool) {
    ......
    old := gp.stack
    ......
    used := old.hi - gp.sched.sp

    // allocate new stack
    new := stackalloc(uint32(newsize))
    ......
    // Compute adjustment.
    var adjinfo adjustinfo
    adjinfo.old = old
    adjinfo.delta = new.hi - old.hi //用於舊棧指標的調整

    //後面有機會和 select / chan 一起分析
    // Adjust sudogs,synchronizing with channel ops if necessary.
    ncopy := used
    if sync {
        adjustsudogs(gp,&adjinfo)
    } else {
        ......
        adjinfo.sghi = findsghi(gp,old)

        // Synchronize with channel ops and copy the part of
        // the stack they may interact with.
        ncopy -= syncadjustsudogs(gp,used,&adjinfo)
    }
    //把舊棧資料複製到新棧
    // Copy the stack (or the rest of it) to the new location
    memmove(unsafe.Pointer(new.hi-ncopy),unsafe.Pointer(old.hi-ncopy),ncopy)

    // Adjust remaining structures that have pointers into stacks.
    // We have to do most of these before we traceback the new
    // stack because gentraceback uses them.
    adjustctxt(gp,&adjinfo)
    adjustdefers(gp,&adjinfo)
    adjustpanics(gp,&adjinfo)
    ......
    // Swap out old stack for new one
    gp.stack = new
    gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request
    gp.sched.sp = new.hi - used
    gp.stktopsp += adjinfo.delta
    // Adjust pointers in the new stack.
    gentraceback(^uintptr(0),^uintptr(0),gp,nil,0x7fffffff,adjustframe,noescape(unsafe.Pointer(&adjinfo)),0)
    ......
    //釋放舊棧
    stackfree(old)
}
複製程式碼

棧幀調整

golang棧幀
package main

func myFunction(a,b int) (int,int) {
    return a + b,a - b
}

func main() {
    myFunction(66,77)
}
複製程式碼

棧幀調整

gentraceback裡回撥了adjustframe函式,我們所需要了解的即golang的棧空間中,有存放函式引數,返回值,函式返回地址等資訊,這些地址都需要調節,該函式就是針對原來的棧指標進行的調節。程式碼如下:

// Note: the argument/return area is adjusted by the callee.
func adjustframe(frame *stkframe,arg unsafe.Pointer) bool {
	adjinfo := (*adjustinfo)(arg)
	targetpc := frame.continpc
	if targetpc == 0 {
		// Frame is dead.
		return true
	}
	f := frame.fn
    .........
	pcdata := pcdatavalue(f,_PCDATA_StackMapIndex,targetpc,&adjinfo.cache)
	if pcdata == -1 {
		pcdata = 0 // in prologue
	}

	// Adjust local variables if stack frame has been allocated.
	size := frame.varp - frame.sp
	var minsize uintptr
	switch sys.ArchFamily {
	case sys.ARM64:
		minsize = sys.SpAlign
	default:
		minsize = sys.MinFrameSize
	}
	if size > minsize {
		var bv bitvector
		stackmap := (*stackmap)(funcdata(f,_FUNCDATA_LocalsPointerMaps))
		if stackmap == nil || stackmap.n <= 0 {
			print("runtime: frame ",funcname(f)," untyped locals ",hex(frame.varp-size),"+",hex(size),"\n")
			throw("missing stackmap")
		}
		// Locals bitmap information,scan just the pointers in locals.
		if pcdata < 0 || pcdata >= stackmap.n {
			print("runtime: pcdata is ",pcdata," and ",stackmap.n," locals stack map entries for "," (targetpc=",")\n")
			throw("bad symbol table")
		}
		bv = stackmapdata(stackmap,pcdata)
		size = uintptr(bv.n) * sys.PtrSize
		if stackDebug >= 3 {
			print("      locals ","/"," ",size/sys.PtrSize," words ",bv.bytedata,"\n")
		}
		adjustpointers(unsafe.Pointer(frame.varp-size),&bv,adjinfo,f)
	}

	// Adjust saved base pointer if there is one.
	if sys.ArchFamily == sys.AMD64 && frame.argp-frame.varp == 2*sys.RegSize {
		if !framepointer_enabled {
			print("runtime: found space for saved base pointer,but no framepointer experiment\n")
			print("argp=",hex(frame.argp)," varp=",hex(frame.varp),"\n")
			throw("bad frame layout")
		}
		if stackDebug >= 3 {
			print("      saved bp\n")
		}
		if debugCheckBP {
			// Frame pointers should always point to the next higher frame on
			// the Go stack (or be nil,for the top frame on the stack).
			bp := *(*uintptr)(unsafe.Pointer(frame.varp))
			if bp != 0 && (bp < adjinfo.old.lo || bp >= adjinfo.old.hi) {
				println("runtime: found invalid frame pointer")
				print("bp=",hex(bp)," min=",hex(adjinfo.old.lo)," max=",hex(adjinfo.old.hi),"\n")
				throw("bad frame pointer")
			}
		}
		adjustpointer(adjinfo,unsafe.Pointer(frame.varp))
	}

	// Adjust arguments.
	if frame.arglen > 0 {
		var bv bitvector
		if frame.argmap != nil {
			bv = *frame.argmap
		} else {
			stackmap := (*stackmap)(funcdata(f,_FUNCDATA_ArgsPointerMaps))
			if stackmap == nil || stackmap.n <= 0 {
				print("runtime: frame "," untyped args ",frame.argp,frame.arglen,"\n")
				throw("missing stackmap")
			}
			if pcdata < 0 || pcdata >= stackmap.n {
				print("runtime: pcdata is "," args stack map entries for ",")\n")
				throw("bad symbol table")
			}
			bv = stackmapdata(stackmap,pcdata)
		}
		if stackDebug >= 3 {
			print("args\n")
		}
		adjustpointers(unsafe.Pointer(frame.argp),funcInfo{})
	}
	return true
}
複製程式碼