• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • Golang 協程Goroutine到底是怎么回事?(一)

    標簽: golang  協程  云計算  對象存儲  

    Golang號稱云計算時代的C語言,是非常值得研究的一門語言

    本文是筆者在初學Golang的時候,學習的一些新的分享。現在開一個系列,Golang究竟怎么回事系列?談Goroutine,談數據結構,不僅語言語義理解,還要更深入的,更本質的看到,Golang的數據結構到底是怎么回事?

    其中,使用到gdb,dlv等調試工具,有此經驗的更佳。(旁白:這也是我更喜歡Golang的原因,可以使用gdb撥開云霧,看到最本質的東西)

    Goroutine思考幾個問題

    1. 協程是什么,協程應用場景?

    2. 協程的調度實現有哪幾種樣式?有哪些常見的協程實現?

    3. 實現一個簡易協程調度

    4. 協程上最重要的準則是什么?

    5. 有了協程要配套哪些東西?

    前面有兩篇介紹協程的文章:

    1. 同步框架異步化改造—任務協程化 (一)

    2. 同步框架異步化改造—任務協程化 (二)

    從簡單的講起,協程是什么?

    協程是什么?

    協程是什么? 協程就是用戶態的最小調度執行單位,類比理解就是用戶態線程,本質就是用戶態自己切換cpu,在協程這一層我們基本可以把線程和cpu等同起來。(旁白:協程這個執行體操作系統是不認識的,只有用戶自己認識,所以你用pstack看線程的工具是看不了協程的)

    協程應用場景?

    1. IO密集型:IO密集型程序,cpu利用率低,使用協程,可以讓用戶按照實際情況調度,充分利用cpu,在當前多核cpu的架構中非常重要

    2. 框架改造:原本項目全是同步調用,cpu利用率低。直接改成異步回調不現實,通過實現協程,達到非侵入式的框架異步改造

    3. 協程的實現使用會使得全異步框架代碼的編寫簡單,可維護性好

    (旁白:協程兩個用法:1)框架同步改造異步    2)異步代碼寫成同步樣子)

    協程的調度實現一般有哪幾種樣式?

    協程最根本的就兩種類型:

    1. 對稱的切換調度方式

    2. 非對稱的切換調度方式

    對稱的調度方式

    每個協程任務都是一樣的,不存在主次,都可以相互切換。這類調度類型看著美觀,但是實現起來會非常復雜,如果加上一些協程鎖,異步io切換邏輯之后,而且極容易出錯。不容易實現時序的串行化。

    image

    非對稱的調度方式

    最典型的就是有一個中心調度任務。主要角色分為:

    1. 主協程:負責所有的協程調度

    2. 任務協程:執行具體的業務邏輯代碼的協程任務

    基本原則:

    1. 嚴格保證所有的協程切換都必須且只能在 “主協程”<-> "任務協程” 之間進行

    2. 存在串行邏輯的時候,必須保證嚴格的串行時序(這個會在協程鎖的實現里講)

    image

    有哪些常見的協程實現?

    Linux提供了協程庫,可以基于以下這四個調用實現協程切換

    #include <ucontext.h>
    void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
    int  swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
    int  getcontext(ucontext_t *ucp);
    int  setcontext(const ucontext_t *ucp);
    
    1. glusterfs

    2. qemu,等

    或者你可以自己保存,交換寄存器棧環境:

    1. libco (C++)

    2. greenlet(Python),等

    怎么實現一個簡易的協程調度?

    image

    上圖是一個比較完整的切換示意圖:

    1. 主協程(調度協程)總是從協程隊列中取出協程任務執行

    2. 協程任務執行過程中,遇到等待事件,需要保存好上下文,設置好喚醒路徑之后,切回調度

    3. 切回調度之后,CPU就讓出來了,就可以執行其他的任務,從而實現了并發

    4. 等待事件到來之后,按照之前設置好的環境路徑,把協程任務再次投入到協程隊列尾端,等待執行

    5. 等重新取到協程的時候,主協程切入,從之前切出的地方開始執行

    以上就是實現的一個簡單的協程調度的原理。當然具體細節會有很多,狀態修改,協程生命周期,校驗邏輯。比如必須:

    1. 加入爆棧的校驗(支不支持棧的自動擴容)

    2. 協程的生命周期的校驗

    3. 可能還需要做一些調試工具,比如查看某個協程的協程函數調用棧

    4. 死鎖檢查

    5. 比如,某個協程加了mutex阻塞鎖,走到后面代碼,就直接切到調度,那么后面一旦有協程任務來加同一把mutex鎖,就會導致死鎖問題

    協程上最重要的準則是什么?

    協程任務上一定不能跑阻塞的任務調用。一定要確保cpu不停的轉。因為所有的協程當前本質上是不支持搶占任務的,因為沒有時間片的概念。一旦阻塞,會導致這個線程執行所有的任務阻塞。

    協程要配套哪些東西?

    1. 協程鎖,條件變量,sleep,或者其他一切和阻塞有關的調用。

    Goroutine的設計

    前面復習完了協程通用的知識,下面終于到了重點戲碼——Golang的協程是怎么回事? (旁白:協程實現很簡單,就四板斧:任務,隊列,切換上下文的手段,代碼執行者)
    G-P-M的數據結構

    image

    作為Go的最大宣傳特點,來看看goroutine的協程實現。goroutine本質上和上面我實現的協程是一樣的。但是由于做了一些層次抽象,更具靈活性。

    • G:Goroutine,一個G就是我們協程任務,是調度執行的單位。所以最重要的就是棧結構了(旁白:四板斧之一:任務)

    • M:Machine,這是一個抽象出來的數據結構,可以認為就是執行體,就是線程,就是cpu,每個M都代表一個線程(旁白:四板斧之一:執行者)

    • P:processor。處理器,這個可以認為就是代表一個硬件cpu核心。通常這個數量也就是和cpu核數相同(旁白:四板斧之一:隊列,Golang的設計就是得P者得天下,得隊列者得天下)

    其中啟動開始P就是固定的,M是會增長的,M執行任務必須是綁定到一個P(也就是說,一定要有一個隊列),沒有綁定到P的M就是空閑的,或者游離態的。這樣數據結構(P)和執行(M)分離增加了擴展性。

    舉兩個例子:

    1. 如果M被阻塞,這個時候,隊列里面所有的G都是要移交出去的,之前會存在比較復雜的操作。GMP架構,只需要M釋放P,空閑的M去接管P就行了。

    2. 如果當前M執行完了P隊列的所有任務,那么也不會空閑等待,而是會嘗試去steal其他的G。先嘗試從全局隊列里獲取,沒有獲取到,那么再去隨機挑選一個P隊列,拿走部分的G。(worke-steal)

    這個GMP的設計是在Go1.1之后加入的:https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.mmq8lm48qfcw

    提一下:go里面實現一些并發同步操作的時候,很多都是使用原子操作來替代鎖,從而減少消耗,這個值得我們學習。

    有些特殊的M,比如sysmon是不綁定P的。這個用于監控一些阻塞的異常情況,比如一個M長時間阻塞超過10ms,那么強制把M-P解綁,把M游離出去,P綁定到一個空閑的M上,繼續執行隊列里的G任務。

    Go程序啟動

    // The bootstrap sequence is:
    //
    // call osinit
    // call schedinit
    // make & queue new G
    // call runtime·mstart
    //
    // The new G calls runtime·main.
    
    1. 做一些初始化的操作

    2. 創建出一個goroutine結構 runtime.main 函數

    3. 執行runtime.mstart 函數

    4. 匯編引導結束,之后就由golang的函數main入口運行

    image.gif

    初始化的時候,會創建幾個線程(M)

    1. sysmon特殊線程

    2. 垃圾回收的線程

    (旁白:goroutine有runtime的運行邏輯)

    Goroutine調度

    image

    創建goroutine

    接口

    newproc
    

    goroutine的調度跟之前我實現的協程調度核心是一致的,但是由于是多了一個抽象層(GPM),靈活性和擴展性大大提高。

    1. go語言里面go關鍵字用于創建goroutine(協程),實際調用的是newproc函數

    2. newproc創建出一個goroutine結構體:G,分配2kb的協程棧(在systemstack環境下調用)

    3. 然后把G加入P隊列中,等待執行

    4. 切回原來的goroutine執行指令

    **步驟一:**用來創建goroutine的結構

    type funcval struct {
       fn uintptr
       // variable-size, fn-specific data here
    }
    

    image

    注意:特意標紅的地方,這里是goroutine調度的一個關鍵。在goroutine執行完fn函數之后,在執行ret匯編指令的時候,會把這個地址取出來放到指令計數器(pc)去執行,而這個地址恰好是goexit的地址。這個賦值就是在newproc的時候賦值的。執行了goexit,你才能切回調度里(非對稱中心化調度)。

    newproc -> newproc1 -> gostartcallfn
    

    **步驟二: **newproc是在systemstack的包裝下調用的,這個調用保證newproc的函數執行是在調度協程的棧里面(M.g0棧)

    
    // func systemstack(fn func())
    TEXT runtime·systemstack(SB), NOSPLIT, $0-8
    ...
    // 切換到調度: switch to g0
    MOVQ   DX, g(CX)
    MOVQ   (g_sched+gobuf_sp)(DX), BX
    SUBQ   $8, BX
    MOVQ   $runtime·mstart(SB), DX
    MOVQ   DX, 0(BX)
    MOVQ   BX, SP
    
    // 執行函數:call target function
    MOVQ   DI, DX
    MOVQ   0(DI), DI
    CALL   DI
    
    // 切回原來的協程:switch back to g
    MOVQ   g(CX), AX
    MOVQ   g_m(AX), BX
    MOVQ   m_curg(BX), AX
    MOVQ   AX, g(CX)
    MOVQ   (g_sched+gobuf_sp)(AX), SP
    MOVQ   $0, (g_sched+gobuf_sp)(AX)
    

    這個就符合中心調度的設計思想。解釋幾個函數調用

    runqget  // goroutine 出隊
    runqput  // goroutine 入隊
    runqgrab // goroutine 搶占
    

    G入隊的幾個優先級:

    1. runqput

    2. p.runqnext (第一優先級)

    3. p.(runqhead, runqtail) 雙端隊列

    4. runqputslow

    5. sched.runq 全局隊列 (p隊列滿了就會溢出到全局隊列,p隊列256個槽位)

    newproc -> newproc1 -> systemstack ( runqput )
    

    步驟三:執行goroutine

    調度接口入口

    schedule
    

    流程就是:

    1. 從隊列里獲取到G

    2. 從P隊列里獲取G任務

    3. 第二優先級從其他地方獲取

    4. 切入執行

    這里提到一點細節就是:go的調度機制是,當執行了n(61)個任務之后,必須要去全局列表獲取G任務,保證公平執行。

    具體切入執行某個G

    execute -> gogo
    

    其中gogo的代碼

    image

    goroutine的搶占調度

    goroutine本質上是沒有搶占式的調用,只是會在goroutine結構體上加上一個標記。因為沒有時間片。只有當有機會調用到特定的調用的時候,才可能發生切出。

    goroutine的自動擴容

    1. 編譯器分析判斷是否可能會導致2kb的棧溢出,如果可能,那么就會在函數的匯編代碼前后加上指令代碼

    2. 前面——判斷是否棧溢出

    3. 后——棧擴容調用morestack

    (旁白:自動擴容的觸發機制也被復用在搶占調度了)

    goroutine的主動切出

    1. Gosched : 把當前G放入到隊列中,然后切出

    2. gopark/goparkunlock : 保存上下文,直接切出

    3. goready : 喚醒G(把G重新入隊)

    image


    堅持思考,方向比努力更重要。關注我:奇伢云存儲image

    版權聲明:本文為qiya2007原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/qiya2007/article/details/105447358

    智能推薦

    Golang 協程

    Golang 協程 1. CSP 并發模型 2. 協程、線程、進程 3. 并發的實現原理 3.1 用戶級線程模型 3.2 內核級線程模型 3.3 兩級線程模型(即混合型線程模型) 4. G-P-M 模型 5. Golang 并發控制模型 6. gorountine調度時機 7. goroutine的切換點 8. 實例 8.1 go關鍵字開啟新協程 8.2 runtime包 1. CSP 并發模型 ...

    數組刪除其中某個對象的方法

    數組刪除其中的對象或元素,在前端是比較常見的需求。 我現在比較常用的方法如下: 這種方法只適合刪除具有唯一標識的對象。 有沒有想要脫單的小伙伴,加入我們的脫單星球,認識更多優秀的小哥哥小姐姐 特此聲明,星球是免費的,但是創建星球的時候說是必須輸入金額,所以只能先私聊,我再加你免費加入!...

    圖床搭建以及圖床工具的使用

    為什么要用圖床和圖床工具? 比較下面三種md中的圖片url地址(均免費),你會使用哪一種? 選1?由于是本地路徑,文檔分享后給其他人打開后很可能顯示圖片加載失敗。 選2?雖然分享后可以顯示圖片,但能保證加載速度? 選3?我肯定選這種,即兼容2的瀏覽器訪問,又能保證訪問速度。 這樣就可以回答上面的問題了!保證瀏覽器訪問要用圖床,保證加載速度要用圖床工具,又不花錢想想就開心。 除此之外本篇博客還會講解...

    并發編程理論篇

    一、必備知識回顧 計算機又叫電腦,即通電的大腦,發明計算機是為了讓他通電之后能夠像人一樣去工作,并且它比人的工作效率更高,因為可以24小時不間斷 計算機五大組成部分 控制器 運算器 存儲器 輸入設備 輸出設備 計算機的核心真正干活的是CPU(控制器+運算器=中央處理器) 程序要想被計算機運行,它的代碼必須要先由硬盤讀到內存,之后cpu取指再執行 并發 看起來像同時運行的就可以稱之為并發 并行 真正...

    Java LinkedHashMap

    Java LinkedHashMap 前言 Map是我們在實際使用過程中常用的集合,HashMap在Java的實際開發中出鏡率很高,它通過hash算法實現了高效的非線程安全的集合,它有一個缺點就是,用戶插入集合的數據時無序,在我們需要一些有序的map的時候,我們就需要引入另外一個集合:LinkedHashMap。 LinkedHashMap是一個有序的非線程安全的集合,它是HashMap的子類,基...

    猜你喜歡

    Spark Streaming處理文件(本地文件以及hdfs上面的文件)

    標題介紹文件流之前先介紹一下Dstream 下面是來自官網一段的說明,Discretized Streams或DStream是Spark Streaming提供的基本抽象。它表示連續的數據流,可以是從源接收的輸入數據流,也可以是通過轉換輸入流生成的已處理數據流。在內部,DStream由一系列連續的RDD表示,這是Spark對不可變的分布式數據集的抽象(有關更多詳細信息,請參見Spark編程指南)。...

    《痞子衡嵌入式半月刊》 第 8 期

    痞子衡嵌入式半月刊: 第 8 期 這里分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農歷年分二十四節氣,希望在每個交節之日準時發布一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 :《痞子衡嵌入式半月刊: 第 7 期》 嘮兩句 今天是小滿,小滿節氣意味著進入了大幅降水的雨季。痞子...

    (C++)二叉樹的線索化 / 線索二叉樹

    好久不見,朋友們!雖然我知道沒人看我的博客,但我還是想叨逼叨一下。啊,好久沒編程了(其實也就一周沒編),但你們知道,程序員一天不編程那能叫程序員么???雖然我不是程序員哈哈哈哈哈,但還是要有基本素養嘛。 繼續寫二叉樹,給自己立一個flag,就是這幾天要寫完之前沒做完的幾道題,和二叉樹紅黑樹各種樹之類的~~雖然有這個flag,但我還是很實誠地遵從自己的內心,買了一張明天的電影票,等我回來告訴你們好不...

    Linux內存管理:分頁機制

    《Linux內存管理:內存描述之內存節點node》 《Linux內存管理:內存描述之內存區域zone》 《Linux內存管理:內存描述之內存頁面page》 《Linux內存管理:內存描述之高端內存》 《Linux內存管理:分頁機制》 《內存管理:Linux Memory Management:MMU、段、分頁、PAE、Cache、TLB》 目錄 1 分頁機制 1.1 為什么使用多級頁表來完成映射 ...

    Logtail 混合模式:使用插件處理文件日志

    作為一個服務百萬機器的日志采集 agent,Logtail 目前已經提供了包括日志切分、日志解析(完整正則、JSON、分隔符)、日志過濾在內的常見處理功能,能夠應對絕大多數場景的處理需求。但有些時候,由于應用的歷史原因或是本身業務日志的復雜性,單一功能可能無法滿足所采集日志的處理需求,比如: 日志可能不再是單一格式,有可能同時由 JSON 或者分隔符日志組成。 日志格式可能也不固定,不同的業務邏輯...

    精品国产乱码久久久久久蜜桃不卡