<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Golang 并發(fā)模型之 GMP 淺嘗

          共 3093字,需瀏覽 7分鐘

           ·

          2020-10-14 09:04


          從進程談起


          ????進程與線程的區(qū)別是什么?這是一個老生長談的一道面試題。處于不同層面對該問題的理解也大不相同。對于用戶層面來說,進程就是一塊運行起來的程序,線程就是程序里的一些并發(fā)的功能。對于操作系統(tǒng)層面來說,標準回答是“進程是資源分配的最小單位,線程是cpu調(diào)度的最小單位”。接下來先從操作系統(tǒng)層面介紹一下進程與線程。



          進程


          ????在程序啟動時,操作系統(tǒng)會給該程序分配一塊內(nèi)存空間,對于程序但看到的是一整塊連續(xù)的內(nèi)存空間,稱為虛擬內(nèi)存空間,落實到操作系統(tǒng)內(nèi)核則是一塊一塊的內(nèi)存碎片的東西。為的是節(jié)省內(nèi)核空間,方便對內(nèi)存管理。



          ????就這片內(nèi)存空間,又劃分為用戶空間內(nèi)核空間,用戶空間只用于用戶程序的執(zhí)行,若要執(zhí)行各種IO操作,就會通過系統(tǒng)調(diào)用等進入內(nèi)核空間進行操作。每個進程都有自己的PID,可以通過ps命令查看某個進程的pid,進入/proc/可以查看該進程的詳細信息,如cgroup,進程資源大小等信息。






          線程


          ????線程是進程的一個執(zhí)行單元,一個進程可以包含多個線程,只有擁有了線程的進程才會被CPU執(zhí)行,所以一個進程最少擁有一個主線程。



          ????由于多個線程可以共享同一個進程的內(nèi)存空間,線程的創(chuàng)建不需要額外的虛擬內(nèi)存空間,線程之間的切換也就少了如進程切換的切換頁表,切換虛擬地址空間此類的巨大開銷。至于進程切換為什么較大,簡單理解是因為進程切換要保存的現(xiàn)場太多如寄存器,棧,代碼段,執(zhí)行位置等,而線程切換只需要保存線程執(zhí)行的上下文即可。線程的的切換只需要保存線程的執(zhí)行現(xiàn)場(程序計數(shù)器等狀態(tài))保存在該線程的棧里,CPU把棧指針,指令寄存器的值指向下一個線程。相比之下線程更加輕量級。



          ? ? 可以說進程面向的主要內(nèi)容是內(nèi)存分配管理,而線程主要面向的CPU調(diào)度。





          協(xié)程


          ????雖然線程比進程要輕量級,但是每個線程依然占有1M左右的空間,在高并發(fā)場景下非常吃機器內(nèi)存,比如構建一個http服務器,如果一個每來一次請求分配一個線程,請求數(shù)暴增容易OOM,而且線程切換的開銷也是不可忽視的。同時,線程的創(chuàng)建與銷毀同樣是比較大的系統(tǒng)開銷,因為是由內(nèi)核來做的,解決方法也有,可以通過線程池或協(xié)程來解決。


          ????協(xié)程是用戶態(tài)的線程,比線程更加的輕量級,操作系統(tǒng)對其沒有感知,之所以沒有感知是由于協(xié)程處于線程的用戶棧能感知的范圍,是由用戶創(chuàng)建的而非操作系統(tǒng)。




          ????如一個進程可擁有以有多個線程一樣,一個線程也可以擁有多個協(xié)程。協(xié)程之于線程如同線程之于cpu,擁有自己的協(xié)程隊列,每個協(xié)程擁有自己的棧空間,在協(xié)程切換時候只需要保存協(xié)程的上下文,開銷要比內(nèi)核態(tài)的線程切換要小很多。






          GMP模型



          含義


          Goroutine的并發(fā)編程模型基于GMP模型,簡要解釋一下GMP的含義:


          G:表示goroutine,每個goroutine都有自己的棧空間,定時器,初始化的棧空間在2k左右,空間會隨著需求增長。


          M:抽象化代表內(nèi)核線程,記錄內(nèi)核線程棧信息,當goroutine調(diào)度到線程時,使用該goroutine自己的棧信息。


          P:代表調(diào)度器,負責調(diào)度goroutine,維護一個本地goroutine隊列,M從P上獲得goroutine并執(zhí)行,同時還負責部分內(nèi)存的管理。




          模型


          從大體看一下GMP模型。




          M代表一個工作線程,在M上有一個P和G,P是綁定到M上的,G是通過P的調(diào)度獲取的,在某一時刻,一個M上只有一個G(g0除外)。在P上擁有一個G隊列,里面是已經(jīng)就緒的G,是可以被調(diào)度到線程棧上執(zhí)行的協(xié)程,稱為運行隊列。



          接下來看一下程序中GMP的分布。






          ????每個進程都有一個全局的G隊列,也擁有P的本地執(zhí)行隊列,同時也有不在運行隊列中的G。如正處于channel的阻塞狀態(tài)的G,還有脫離P綁定在M的(系統(tǒng)調(diào)用)G,還有執(zhí)行結(jié)束后進入P的gFree列表中的G等等,接下來列舉一下常見的幾種狀態(tài)。




          狀態(tài)匯總


          G狀態(tài)


          G的主要幾種狀態(tài):

          本文基于Go1.13,具體代碼見(/src/runtime/runtime2.go)


          _Gidle:剛剛被分配并且還沒有被初始化,值為0,為創(chuàng)建goroutine后的默認值


          _Grunnable 沒有執(zhí)行代碼,沒有棧的所有權,存儲在運行隊列中,可能在某個P的本地隊列或全局隊列中(如上圖)。


          _Grunning 正在執(zhí)行代碼的goroutine,擁有棧的所有權(如上圖)


          _Gsyscall:正在執(zhí)行系統(tǒng)調(diào)用,擁有棧的所有權,與P脫離,但是與某個M綁定,會在調(diào)用結(jié)束后被分配到運行隊列(如上圖)。?


          _Gwaiting:被阻塞的goroutine,阻塞在某個channel的發(fā)送或者接收隊列(如上圖)


          _Gdead? 當前goroutine未被使用,沒有執(zhí)行代碼,可能有分配的棧,分布在空閑列表gFree,可能是一個剛剛初始化的goroutine,也可能是執(zhí)行了goexit退出的goroutine(如上圖)


          _Gcopystac:棧正在被拷貝,沒有執(zhí)行代碼,不在運行隊列上,執(zhí)行權在


          _Gscan ? GC 正在掃描棧空間,沒有執(zhí)行代碼,可以與其他狀態(tài)同時存在





          P的狀態(tài)


          _Pidle :處理器沒有運行用戶代碼或者調(diào)度器,被空閑隊列或者改變其狀態(tài)的結(jié)構持有,運行隊列為空


          _Prunning :被線程 M 持有,并且正在執(zhí)行用戶代碼或者調(diào)度器(如上圖)


          _Psyscall:沒有執(zhí)行用戶代碼,當前線程陷入系統(tǒng)調(diào)用(如上圖)


          _Pgcstop?:被線程 M 持有,當前處理器由于垃圾回收被停止


          _Pdead? :當前處理器已經(jīng)不被使用





          M的狀態(tài)


          自旋線程:處于運行狀態(tài)但是沒有可執(zhí)行goroutine的線程(如下圖),數(shù)量最多為GOMAXPROC,若是數(shù)量大于GOMAXPROC就會進入休眠。




          非自旋線程:處于運行狀態(tài)有可執(zhí)行goroutine的線程。






          調(diào)度場景


          Channel阻塞:當goroutine讀寫channel發(fā)生阻塞時候,會調(diào)用gopark函數(shù),該G會脫離當前的M與P,調(diào)度器會執(zhí)行schedule函數(shù)調(diào)度新的G到當前M。可參考上一篇文章channel探秘


          系統(tǒng)調(diào)用:當某個G由于系統(tǒng)調(diào)用陷入內(nèi)核態(tài)時,該P就會脫離當前的M,此時P會更新自己的狀態(tài)為Psyscall,M與G互相綁定,進行系統(tǒng)調(diào)用。結(jié)束以后若該P狀態(tài)還是Psyscall,則直接關聯(lián)該M和G,否則使用閑置的處理器處理該G。


          系統(tǒng)監(jiān)控:當某個G在P上運行的時間超過10ms時候,或者P處于Psyscall狀態(tài)過長等情況就會調(diào)用retake函數(shù),觸發(fā)新的調(diào)度。


          主動讓出:由于是協(xié)作式調(diào)度,該G會主動讓出當前的P,更新狀態(tài)為Grunnable,該P會調(diào)度隊列中的G運行。





          總結(jié)


          ??? Go語言中通過GMP模型實現(xiàn)了對CPU和內(nèi)存的合理利用,使得用戶在不用擔心內(nèi)存的情況下體驗到線程的好處。雖說協(xié)程的空間很小,但是也需要關注一下協(xié)程的生命周期,防止過多的協(xié)程滯留造成OOM。最近遇到了線上的OOM問題,等待下期一起分析。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包(下圖只是部分),同時還包含學習建議:入門看什么,進階看什么。

          關注公眾號 「polarisxu」,回復?ebook?獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。







          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲蜜桃精品 | 91人妻人人爽人人澡人人爽 | 91青青草视频在线 | 人人操人人妻 | 亚洲婷婷成人激久久月天 |