中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go協作與搶占怎么實現

發布時間:2023-04-04 10:30:28 來源:億速云 閱讀:130 作者:iii 欄目:開發技術

這篇“Go協作與搶占怎么實現”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Go協作與搶占怎么實現”文章吧。

    1. 用戶主動讓出CPU:runtime.Gosched函數

    在介紹兩種搶占調度之前,我們首先介紹一下runtime.Gosched函數:

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

    根據說明,runtime.Gosched函數會主動放棄當前處理器,并且允許其他協程執行,但是起并不會暫停自己,而只是讓渡調度權,之后依賴調度器獲得重新調度。

    之后,會通過mcall函數切換到g0棧去執行gosched_m函數:

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

    gosched_m調用goschedImpl函數,其會為協程gp讓渡出本M,并且將gp放到全局隊列中,等待調度。

    func goschedImpl(gp *g) {
       status := readgstatus(gp)
       if status&^_Gscan != _Grunning {
          dumpgstatus(gp)
          throw("bad g status")
       }
       casgstatus(gp, _Grunning, _Grunnable)
       dropg()            // 使當前m放棄gp,就是其參數 curg
       lock(&sched.lock)
       globrunqput(gp)    // 并且把gp放到全局隊列中,等待調度
       unlock(&sched.lock)
    
       schedule()
    }

    雖然runtime.Gosched具有主動放棄CPU的能力,但是對用戶的要求比較高,并非用戶友好的。

    2. 基于協作的搶占式調度

    2.1 場景

    package main
    
    import (
       "fmt"
       "runtime"
       "sync"
       "time"
    )
    
    var once = sync.Once{}
    
    func f() {
       once.Do(func() {
          fmt.Println("I am go routine 1!")
       })
    }
    
    func main() {
       defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
    
       go func() {
          for {
             f()
          }
       }()
    
       time.Sleep(10 * time.Millisecond)
       fmt.Println("I am main goroutine!")
    }

    我們考慮如上代碼,首先我們設置P的個數為1,然后起一個協程中進入死循環,循環調用一個函數,如果沒有搶占調度,那么這個協程將一直占據P,也就是會一直占據CPU,代碼就永遠不可能執行到fmt.Println("I am main goroutine!")這行。下面我們看看,協作式搶占是怎么避免以上問題的。

    2.2 棧擴張與搶占標記

    $ go tool compile -N -l main.go
    $ go tool objdump main.o >> main.i

    我們通過以上指令,得到2.1中代碼的匯編代碼,截取f函數的匯編代碼如下:

    TEXT "".f(SB) gofile../home/chenyiguo/smb_share/go_routine_test/main.go
      main.go:12      0x151a       493b6610      CMPQ 0x10(R14), SP 
      main.go:12      0x151e       762b         JBE 0x154b    
      main.go:12      0x1520       4883ec18      SUBQ $0x18, SP    
      main.go:12      0x1524       48896c2410    MOVQ BP, 0x10(SP)  
      main.go:12      0x1529       488d6c2410    LEAQ 0x10(SP), BP  
      main.go:13      0x152e       488d0500000000    LEAQ 0(IP), AX    [3:7]R_PCREL:"".once      
      main.go:13      0x1535       488d1d00000000    LEAQ 0(IP), BX    [3:7]R_PCREL:"".f.func1·f  
      main.go:13      0x153c       e800000000    CALL 0x1541       [1:5]R_CALL:sync.(*Once).Do    
      main.go:16      0x1541       488b6c2410    MOVQ 0x10(SP), BP  
      main.go:16      0x1546       4883c418      ADDQ $0x18, SP    
      main.go:16      0x154a       c3       RET          
      main.go:12      0x154b       e800000000    CALL 0x1550       [1:5]R_CALL:runtime.morestack_noctxt   
      main.go:12      0x1550       ebc8         JMP "".f(SB)  

    其中第一行,CMPQ 0x10(R14), SP就是比較SP0x10(R14)(其實就是stackguard0)的大小(注意AT&T格式下CMP系列指令的順序),當SP小于等于0x10(R14)時,就會調轉到0x154b地址調用runtime.morestack_noctxt,觸發棧擴張操作。其實如果你仔細觀察就會發現,所有的函數的序言(函數調用的最前方)都被插入了檢測指令,除非在函數上標記//go:nosplit

    接下來,我們將關注于兩點來打通整個鏈路,即:

    • 棧擴張怎么重新調度,讓出CPU的執行權?

    • 何時會設置棧擴張標記?

    2.3 棧擴張怎么觸發重新調度

    // morestack but not preserving ctxt.
    TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
       MOVL   $0, DX
       JMP    runtime·morestack(SB)
    
    TEXT runtime·morestack(SB),NOSPLIT,$0-0
       ...
    
       // Set g->sched to context in f.
       MOVQ   0(SP), AX // f's PC
       MOVQ   AX, (g_sched+gobuf_pc)(SI)
       LEAQ   8(SP), AX // f's SP
       MOVQ   AX, (g_sched+gobuf_sp)(SI)
       MOVQ   BP, (g_sched+gobuf_bp)(SI)
       MOVQ   DX, (g_sched+gobuf_ctxt)(SI)
    
       ...
       CALL   runtime·newstack(SB)
       CALL   runtime·abort(SB)  // crash if newstack returns
       RET

    以上代碼中,runtime·morestack_noctxt調用runtime·morestack,在runtime·morestack中,會首先記錄協程的PC和SP,然后調用runtime.newstack

    func newstack() {
       ...
    
       gp := thisg.m.curg
       
       ...
       stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
    
       ...
       preempt := stackguard0 == stackPreempt
       ...
    
       if preempt {
          if gp == thisg.m.g0 {
             throw("runtime: preempt g0")
          }
          if thisg.m.p == 0 && thisg.m.locks == 0 {
             throw("runtime: g is running but p is not")
          }
    
          if gp.preemptShrink {
             // We're at a synchronous safe point now, so
             // do the pending stack shrink.
             gp.preemptShrink = false
             shrinkstack(gp)
          }
    
          if gp.preemptStop {
             preemptPark(gp) // never returns
          }
    
          // Act like goroutine called runtime.Gosched.
          gopreempt_m(gp) // never return
       }
    
       ...
    }

    我們簡化runtime.newstack函數,總結起來就是通過現有工作協程的stackguard0字段,來判斷是不是應該發生搶占,如果需要的話,則調用gopreempt_m(gp)函數:

    func gopreempt_m(gp *g) {
       if trace.enabled {
          traceGoPreempt()
       }
       goschedImpl(gp)
    }

    可以看到,gopreempt_m函數和前面講到Gosched函數時說到的gosched_m函數一樣,都將調用goschedImpl函數,為協程gp讓渡出本M,并且將gp放到全局隊列中,等待調度。

    這里我們就明白了,一旦發生棧擴張,就有可能會發生讓渡出執行權,進行重新調度的可能性,那什么時候會發生棧擴張呢?

    2.4 何時設置棧擴張標記

    在代碼中,將stackguard0字段置為stackPreempt的地方有不少,但是和我們以上場景相符的還是在后臺監護線程sysmon循環中,對于陷入系統調用和長時間運行的goroutine的運行權進行奪取的retake函數:

    func sysmon() {
       ...
    
       for {
          ...
          // retake P's blocked in syscalls
          // and preempt long running G's
          if retake(now) != 0 {
             idle = 0
          } else {
             idle++
          }
          ...
       }
    }
    func retake(now int64) uint32 {
       ...
       for i := 0; i < len(allp); i++ {
          ...
          s := _p_.status
          sysretake := false
          if s == _Prunning || s == _Psyscall {
             // Preempt G if it's running for too long.
             t := int64(_p_.schedtick)
             if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
             } else if pd.schedwhen+forcePreemptNS <= now { // forcePreemptNS=10ms
                preemptone(_p_) // 在這里設置棧擴張標記
                // In case of syscall, preemptone() doesn't
                // work, because there is no M wired to P.
                sysretake = true
             }
          }
          ...
       }
       unlock(&allpLock)
       return uint32(n)
    }

    其中,在preemptone函數中進行棧擴張標記的設置:

    func preemptone(_p_ *p) bool {
       mp := _p_.m.ptr()
       if mp == nil || mp == getg().m {
          return false
       }
       gp := mp.curg
       if gp == nil || gp == mp.g0 {
          return false
       }
    
       gp.preempt = true
    
       // Every call in a goroutine checks for stack overflow by
       // comparing the current stack pointer to gp->stackguard0.
       // Setting gp->stackguard0 to StackPreempt folds
       // preemption into the normal stack overflow check.
       gp.stackguard0 = stackPreempt // 設置棧擴張標記
    
       // Request an async preemption of this P.
       if preemptMSupported && debug.asyncpreemptoff == 0 {
          _p_.preempt = true
          preemptM(mp)
       }
    
       return true
    }

    通過以上,我們串通起了goroutine協作式搶占的邏輯:

    • 首先,后臺監控線程會對運行時間過長(&ge;10ms)的協程設置棧擴張標記;

    • 協程運行到任何一個函數的序言的時候,都會首先檢查棧擴張標記;

    • 如果需要進行棧擴張,在進行棧擴張的時候,會奪取這個協程的運行權,從而實現搶占式調度。

    3. 基于信號的搶占式調度

    分析以上結論我們可以知道,上述搶占觸發邏輯有一個致命的缺點,那就是必須要運行到函數棧的序言部分,而這根本無法讀取以下協程的運行權,在Go的1.14版本之前,一下代碼不會打印最后一句"I am main goroutine!"

    package main
    
    import (
       "fmt"
       "runtime"
       "sync"
       "time"
    )
    
    var once = sync.Once{}
    
    func main() {
       defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
    
       go func() {
          for {
             once.Do(func() {
                fmt.Println("I am go routine 1!")
             })
          }
       }()
    
       time.Sleep(10 * time.Millisecond)
       fmt.Println("I am main goroutine!")
    }

    因為以上協程中的for循環是個死循環,且并不會包含棧擴張邏輯,所以不會讓渡出自身的執行權。

    3.1 發送搶占信號

    為此,Go SDK引入了基于信號的搶占式調度。我們注意分析上一節preemptone函數代碼中有以下部分:

    if preemptMSupported && debug.asyncpreemptoff == 0 {
       _p_.preempt = true
       preemptM(mp)
    }

    其中preemptM函數會發送_SIGURG信號給需要搶占的線程:

    const sigPreempt = _SIGURG
    
    
    func preemptM(mp *m) {
       // On Darwin, don't try to preempt threads during exec.
       // Issue #41702.
       if GOOS == "darwin" || GOOS == "ios" {
          execLock.rlock()
       }
    
       if atomic.Cas(&mp.signalPending, 0, 1) {
          if GOOS == "darwin" || GOOS == "ios" {
             atomic.Xadd(&pendingPreemptSignals, 1)
          }
    
          // If multiple threads are preempting the same M, it may send many
          // signals to the same M such that it hardly make progress, causing
          // live-lock problem. Apparently this could happen on darwin. See
          // issue #37741.
          // Only send a signal if there isn't already one pending.
          signalM(mp, sigPreempt)
       }
    
       if GOOS == "darwin" || GOOS == "ios" {
          execLock.runlock()
       }
    }

    3.2 搶占調用的注入

    說到這里,我們就需要回到最開始,在第一個協程m0開啟mstart的調用鏈路上,會調用mstartm0函數,在這里會調用initsig

    func initsig(preinit bool) {
      ...
    
       for i := uint32(0); i < _NSIG; i++ {
          ...
    
          handlingSig[i] = 1
          setsig(i, abi.FuncPCABIInternal(sighandler))
       }
    }

    在以上,注冊了sighandler函數:

    func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
       ...
    
       if sig == sigPreempt && debug.asyncpreemptoff == 0 {
          // Might be a preemption signal.
          doSigPreempt(gp, c)
          // Even if this was definitely a preemption signal, it
          // may have been coalesced with another signal, so we
          // still let it through to the application.
       }
    
       ...
    }

    然后接收到sigPreempt信號時,會通過doSigPreempt函數處理如下:

    func doSigPreempt(gp *g, ctxt *sigctxt) {
       // Check if this G wants to be preempted and is safe to
       // preempt.
       if wantAsyncPreempt(gp) {
          if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
             // Adjust the PC and inject a call to asyncPreempt.
             ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc) // 插入搶占調用
          }
       }
    
       // Acknowledge the preemption.
       atomic.Xadd(&gp.m.preemptGen, 1)
       atomic.Store(&gp.m.signalPending, 0)
    
       if GOOS == "darwin" || GOOS == "ios" {
          atomic.Xadd(&pendingPreemptSignals, -1)
       }
    }

    最終,doSigPreempt&mdash;>asyncPreempt->asyncPreempt2

    func asyncPreempt2() {
       gp := getg()
       gp.asyncSafePoint = true
       if gp.preemptStop {
          mcall(preemptPark)
       } else {
          mcall(gopreempt_m)
       }
       gp.asyncSafePoint = false
    }

    然后,又回到了我們熟悉的gopreempt_m函數,這里就不贅述了。

    所以對于基于信號的搶占調度,總結如下:

    • M1發送信號_SIGURG

    • M2接收到信號,并通過信號處理函數進行處理;

    • M2修改執行的上下文,并恢復到修改后的位置;

    • 重新進入調度循環,進而調度其他goroutine

    以上就是關于“Go協作與搶占怎么實現”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    go
    AI

    岑巩县| 新源县| 巴彦淖尔市| 阳原县| 辽阳市| 汕头市| 兴山县| 镇江市| 如东县| 威宁| 都江堰市| 泰宁县| 衡水市| 聂荣县| 邹城市| 曲沃县| 揭东县| 论坛| 泉州市| 金堂县| 尚志市| 宝兴县| 阿勒泰市| 齐齐哈尔市| 大石桥市| 德州市| 新津县| 平果县| 天全县| 永顺县| 始兴县| 偃师市| 安吉县| 邮箱| 嘉黎县| 隆昌县| 龙井市| 镶黄旗| 昭通市| 依兰县| 远安县|