日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

golang var 初始化时机_你应该知道的 Go 调度器知识:Go 核心原理 — 协程调度时机...

發布時間:2025/4/16 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 golang var 初始化时机_你应该知道的 Go 调度器知识:Go 核心原理 — 协程调度时机... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

點擊上方藍色“Go語言中文網”關注我們,領全套Go資料,每天學習?Go?語言

本文作者:葉不聞

原文鏈接:https://juejin.im/post/5dafc241f265da5ba95c465d

golang 調度模型

模型總攬

核心實體

Goroutines (G)

golang 調度單元,golang 可以開啟成千上萬個 g,每個 g 可以理解為一個任務,等待被調度。其存儲了 goroutine 的執行 stack 信息、goroutine 狀態以及 goroutine 的任務函數等。g 只能感知到 p,下文說的 m 對其透明的。

OSThread (M)

系統線程,實際執行 g 的狠角色,但 m 并不維護 g 的狀態,一切都是由幕后黑手 p 來控制。

Processor (P)

維護 m 執行時所需要的上下文,p 的個數通常和 cpu 核數一致(可以設置),代表 gorotine 的并發度。其維護了 g 的隊列。

實體間的關系

一圖勝千言,直接看這個經典的圖

調度本質

即 schedule 函數,通過調度,放棄目前執行的 g,選擇一個 g 來執行。選擇算法不是本文重點,這里不做過多講述。

切換時機

  • 會阻塞的系統調用,比如文件 io,網絡 io;
  • time 系列定時操作;
  • go func 的時候, func 執行完的時候;
  • 管道讀寫阻塞的情況;
  • 垃圾回收之后。
  • 主動調用 runtime.Gosched()

調度時機分析

阻塞性系統調用

系統調用,如 read,golang 重寫了所有系統調用,在系統調用加入了調度邏輯。

拿 read 舉例

/usr/local/go/src/os/file.go:97

// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and an error, if any.
// EOF is signaled by a zero count with err set to io.EOF.
func (f *File) Read(b []byte) (n int, err error) {
if f == nil {
return 0, ErrInvalid
}
n, e := f.read(b)
if n == 0 && len(b) > 0 && e == nil {
return 0, io.EOF
}
if e != nil {
err = &PathError{"read", f.name, e}
}
return n, err
}

嵌套到幾層,就不全部貼出來,跟到底是如下函數:

func read(fd int, p []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_READ, uintptr(fd), uintptr(_p0), uintptr(len(p)))
n = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

Syscall 是匯編實現

TEXT ·Syscall(SB),NOSPLIT,$0-56
BL runtime·entersyscall(SB)
MOVD a1+8(FP), R3
MOVD a2+16(FP), R4
MOVD a3+24(FP), R5
MOVD R0, R6
MOVD R0, R7
MOVD R0, R8
MOVD trap+0(FP), R9 // syscall entry
SYSCALL R9
BVC ok
MOVD $-1, R4
MOVD R4, r1+32(FP) // r1
MOVD R0, r2+40(FP) // r2
MOVD R3, err+48(FP) // errno
BL runtime·exitsyscall(SB)
RET
ok:
MOVD R3, r1+32(FP) // r1
MOVD R4, r2+40(FP) // r2
MOVD R0, err+48(FP) // errno
BL runtime·exitsyscall(SB)
RET

可以看到,進入系統調用時,是調用 entersyscall,當離開系統調用,會運行 exitsyscall

// Standard syscall entry used by the go syscall library and normal cgo calls.
//go:nosplit
func entersyscall(dummy int32) {
reentersyscall(getcallerpc(unsafe.Pointer(&dummy)), getcallersp(unsafe.Pointer(&dummy)))
}

func reentersyscall(pc, sp uintptr) {
_g_ := getg()

// Disable preemption because during this function g is in Gsyscall status,
// but can have inconsistent g->sched, do not let GC observe it.
_g_.m.locks++

// Entersyscall must not call any function that might split/grow the stack.
// (See details in comment above.)
// Catch calls that might, by replacing the stack guard with something that
// will trip any stack check and leaving a flag to tell newstack to die.
_g_.stackguard0 = stackPreempt
_g_.throwsplit = true

// Leave SP around for GC and traceback.
save(pc, sp)
_g_.syscallsp = sp
_g_.syscallpc = pc
casgstatus(_g_, _Grunning, _Gsyscall)
if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
systemstack(func() {
print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
throw("entersyscall")
})
}

if trace.enabled {
systemstack(traceGoSysCall)
// systemstack itself clobbers g.sched.{pc,sp} and we might
// need them later when the G is genuinely blocked in a
// syscall
save(pc, sp)
}

if atomic.Load(&sched.sysmonwait) != 0 { // TODO: fast atomic
systemstack(entersyscall_sysmon)
save(pc, sp)
}

if _g_.m.p.ptr().runSafePointFn != 0 {
// runSafePointFn may stack split if run on this stack
systemstack(runSafePointFn)
save(pc, sp)
}

_g_.m.syscalltick = _g_.m.p.ptr().syscalltick
_g_.sysblocktraced = true
_g_.m.mcache = nil
_g_.m.p.ptr().m = 0
atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
if sched.gcwaiting != 0 {
systemstack(entersyscall_gcwait)
save(pc, sp)
}

// Goroutines must not split stacks in Gsyscall status (it would corrupt g->sched).
// We set _StackGuard to StackPreempt so that first split stack check calls morestack.
// Morestack detects this case and throws.
_g_.stackguard0 = stackPreempt
_g_.m.locks--
}

進入系統調用時,p 和 m 分離,當前運行的 g 狀態變為 _Gsyscall。

_Gsyscall 恢復時機:

  • 當 m 執行完,調用 exitsyscall 重新和之前的 p 綁定,其中調度的還是 schedule 函數;
  • sysmon 線程,發現該 p 一定時間沒有執行,會其分配一個新的 m。此時進入調度。
  • time 定時類操作

    都拿 time.Sleep 舉例

    // Sleep pauses the current goroutine for at least the duration d.
    // A negative or zero duration causes Sleep to return immediately.
    func Sleep(d Duration)
    實際定義在runtime
    // timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
    //go:linkname timeSleep time.Sleepfunc timeSleep(ns int64) {
    if ns <= 0 {
    return
    }

    t := getg().timer
    if t == nil {
    t = new(timer)
    getg().timer = t
    }
    *t = timer{}
    t.when = nanotime() + ns
    t.f = goroutineReady
    t.arg = getg()
    lock(&timers.lock)
    addtimerLocked(t)
    goparkunlock(&timers.lock, "sleep", traceEvGoSleep, 2)
    }

    goparkunlock 最終調用 gopark

    func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
    mp := acquirem()
    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)
    }

    mcall(fn) 是切換到 g0,讓 g0 來調用 fn,這里我們看下 park_m 定義 park_m

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

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

    casgstatus(gp, _Grunning, _Gwaiting)
    dropg()

    if _g_.m.waitunlockf != nil {
    fn := *(*func(*g, unsafe.Pointer) bool)(unsafe.Pointer(&_g_.m.waitunlockf))ok := fn(gp, _g_.m.waitlock)
    _g_.m.waitunlockf = nil
    _g_.m.waitlock = nilif !ok {
    if trace.enabled {
    traceGoUnpark(gp, 2)
    }
    casgstatus(gp, _Gwaiting, _Grunnable)
    execute(gp, true) // Schedule it back, never returns.
    }
    }
    schedule()
    }

    可以看到,先把狀態轉化為 _Gwaiting, 再進行了一次 schedule 針對 _Gwaiting 的 g,需要調用 goready,才能恢復。

    新起一個協程和退出

    新開一個協程,g 狀態會變為 _GIdle,觸發調度。當協程執行完,會調用 goexit1 此時狀態變為 _GDead,_Gdead 可以被復用,或者被 gc 清除。

    管道阻塞

    chansend 即 c 的實現

    func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    if c == nil {
    if !block {
    returnfalse
    }
    gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
    throw("unreachable")
    }

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

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

    ........
    // 省略無關代碼
    ........

    // Block on the channel. Some receiver will complete our operation for us.
    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
    mysg.g = gp
    mysg.selectdone = nil
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    c.sendq.enqueue(mysg)
    goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

    // someone woke us up.
    if mysg != gp.waiting {
    throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    if gp.param == nil {
    if c.closed == 0 {
    throw("chansend: spurious wakeup")
    }
    panic(plainError("send on closed channel"))
    }
    gp.param = nil
    if mysg.releasetime > 0 {
    blockevent(mysg.releasetime-t0, 2)
    }
    mysg.c = nil
    releaseSudog(mysg)
    returntrue
    }

    可以看到,實際還是調用 goparkunlock->gopark,來進行調度。

    gc 之后

    stw 之后,會重新選擇 g 開始執行。此處不對垃圾回收做過多擴展。

    主動調用 runtime.Gosched()

    沒有找到非要調用 runtime.Gosched 的場景,主要作用還是為了調試,學習 runtime 吧

    // Gosched yields the processor, allowing other goroutines to run. It does not
    // suspend the current goroutine, so execution resumes automatically.
    //go:nosplit
    func Gosched() {
    mcall(gosched_m)
    }

    第一步就將環境切換到 g0,然后執行一個叫 gosched_m 的函數

    // Gosched continuation on g0.
    func gosched_m(gp *g) {
    if trace.enabled {
    traceGoSched()
    }
    goschedImpl(gp)
    }

    func goschedImpl(gp *g) {
    status := readgstatus(gp)
    if status&^_Gscan != _Grunning {
    dumpgstatus(gp)
    throw("bad g status")
    }
    casgstatus(gp, _Grunning, _Grunnable)
    dropg()
    lock(&sched.lock)
    globrunqput(gp)
    unlock(&sched.lock)

    schedule()
    }

    可以看到,當前 g 被設置為 _Grunnable,放入執行隊列。然后調用 schedule,選擇一個合適的 g 進行執行。

    總結

    golang 協程調度時機主要是阻塞性操作開始,結束。研究每個場景相關代碼,即可對 golang 有更深的理解。這里也分享一個閱讀源碼的小經驗,每次帶著一個特定問題去尋找答案,比如本文的調度時機,后面再看調度算法,垃圾回收,這樣每次能忽略無關因素,通過多個不同的主題,整個框架會越來越完善。

    參考文章

  • A complete journey with Goroutines: https://medium.com/@riteeksrivastava/a-complete-journey-with-goroutines-8472630c7f5c
  • Go's work-stealing scheduler: https://rakyll.org/scheduler/
  • 推薦閱讀

    • xxx


    喜歡本文的朋友,歡迎關注“Go語言中文網”:

    Go語言中文網啟用微信學習交流群,歡迎加微信:274768166

    總結

    以上是生活随笔為你收集整理的golang var 初始化时机_你应该知道的 Go 调度器知识:Go 核心原理 — 协程调度时机...的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。