Linux角度仰視Goroutine的GMP

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"golang的調度在現在的計算機發展中, 有着開創的意義,依靠於linux kernel的調度基礎,設計出了gmp來供語言級的協程調度,在生產與性能方面,有着卓越的優勢","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"知識回顧","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux 調度","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"調度策略","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux 從2.6.23版本開始,將CFS(Completely Fair Scheduler)默認的調度器。在CFS中實際上維護了一個紅黑樹(在底層維護的一個sched_entity的結構體對象), 把CPU當做一種資源,並記錄下每一個進程對該資源使用的情況,在調度時,調度器總是選擇消耗資源最少的進程來運行。這就是所謂的“完全公平”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Linux內核中還存在其他4中調度器:Stop調度器、deadline調度器、RT調度器和IDLE-Task調度器。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"實現原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實現層面,Linux通過引入virtual runtime(vruntime)來完成上面的設想,具體的,我們來看下從實際運行時間到vruntime的換算公式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"vruntime = 實際運行時間 * 1024 / 進程權重","attrs":{}}],"attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Linux所謂的調度就是調度(進程/線程)","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"進程控制塊","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於Linux調度器實際是識別 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"task_struct","attrs":{}}],"attrs":{}},{"type":"text","text":" 進行調度,而在Linux系統中","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"task_struct","attrs":{}}],"attrs":{}},{"type":"text","text":"就是進程控制塊(PCB),基本數據結構如下:","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"(由於線程又叫做輕量級進程(light-weight process LWP),也有PCB,創建線程使用的底層函數和進程底層一樣,都是clone。所以線程的數據結構不再重複複述。)","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"``` \nstruct task_struct {\n\n\tlong state; /*任務的運行狀態(-1 不可運行,0 可運行(就緒),>0 已停止)*/\n\n\tlong counter;/*運行時間片計數器(遞減)*/\n\n\tlong priority;/*優先級*/\n\n\tlong signal;/*信號*/\n\n\tstruct sigaction sigaction[32];/*信號執行屬性結構,對應信號將要執行的操作和標誌信息*/\n\n\tlong blocked; /* bitmap of masked signals */\n\n\t  /* various fields */\n\n\tint exit_code;/*任務執行停止的退出碼*/\n\n\tunsigned long start_code, end_code, end_data, brk, start_stack;/*代碼段地址 代碼長度(字節數)\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t   代碼長度 + 數據長度(字節數)總長度 堆棧段地址*/\n\n\tlong pid, father, pgrp, session, leader;/*進程標識號(進程號) 父進程號 父進程組號 會話號 會話首領*/\n\n\tunsigned short uid, euid, suid;/*用戶標識號(用戶id) 有效用戶id 保存的用戶id*/\n\n\tunsigned short gid, egid, sgid; /*組標識號(組id) 有效組id 保存的組id*/\n\n\tlong alarm;/*報警定時值*/\n\n\tlong utime, stime, cutime, cstime, start_time;/*用戶態運行時間 內核態運行時間 子進程用戶態運行時間\n\n\t\t\t\t\t\t\t\t\t\t\t\t   子進程內核態運行時間 進程開始運行時刻*/\n\n\tunsigned short used_math;/*標誌:是否使用協處理器*/\n\n\t  /* file system info */\n\n\tint tty; /* -1 if no tty, so it must be signed */\n\n\tunsigned short umask;/*文件創建屬性屏蔽位*/\n\n\tstruct m_inode * pwd;/*當前工作目錄i 節點結構*/\n\n\tstruct m_inode * root;/*根目錄i節點結構*/\n\n\tstruct m_inode * executable;/*執行文件i節點結構*/\n\n\tunsigned long close_on_exec;/*執行時關閉文件句柄位圖標誌*/\n\n\tstruct file * filp[NR_OPEN];/*進程使用的文件表結構*/\n\n\t  /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */\n\n\tstruct desc_struct ldt[3];/*本任務的局部描述符表。0-空,1-代碼段cs,2-數據和堆棧段ds&ss*/\n\n\t  /* tss for this task */\n\n\tstruct tss_struct tss; /*本進程的任務狀態段信息結構*/\n\t\n\tconst struct sched_class *sched_class; /*調度策略的執行邏輯*/\n\t\n\t unsigned int policy; /*調度策略*/\n\n}; \n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出成員變量不少,我提煉一下關鍵的幾個字段作用:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/29/29878f1202af5d06dc48bc800ee11833.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Api:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"system() 啓動一個新進程","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"exec() 以替換當前進程映像的方式啓動一個新進程","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"fork() 以複製當前進程映像的方式啓動一個新進程,除了task_struct和堆棧,子進程和父進程共享所有的資源,相當於複製了一個父進程,但是由於linux採用了寫時複製技術,複製工作不是立即就執行,提高了效率。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"wait() 父進程掛起,等待子進程結束。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"clone() 創建一個線程與當前的進程共享 ","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"pthread_create() 創建一個用戶線程,底層實際上調用clone(),並把開闢一個 stack 作爲參數 thread 建立 , 同步銷燬等由線程庫負責。 ","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏補充:提到線程,這裏順便以Java爲例子:每個Java線程一對一映射到Solaris平臺上的一個本地線程上,並將線程調度交由本地線程的調度程序。由於Java線程是與本地線程是一對一地綁在一起的,而在調Java的SDK","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"setPriority()","attrs":{}}],"attrs":{}},{"type":"text","text":"也不能百分百的修改線程的優先級;","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"runqueue 運行隊列","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux內核使用struct_rq結構來描述運行隊列,結構體如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"struct rq {\n\t\n\traw_spinlock_t lock; /*隊列時鐘 */ \n \n /* 三個調度隊列:CFS調度,RT調度,DL調度 */\n\tstruct cfs_rq cfs;\n\tstruct rt_rq rt;\n\tstruct dl_rq dl;\n\n /* stop指向遷移內核線程, idle指向空閒內核線程 */\n struct task_struct *curr, *idle, *stop;\n \n /* ... */\n} ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rq的基本職責就是:在每一個CPU中都有一個自己的運行隊列,當一個CPU接受到一個task的時候,就會以sched_entity的實體丟入隊列中,最後選擇使用的調度器來進行調度;具體如流程如下圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/39/39dffbb6bc322c9ecde0428c5127f216.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"Schedule函數","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上圖中存在一個schedule的主程序入口,具體的功能如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/13/131e48e2dfd57295b302cd6b5ea96b6c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Java的線程調度","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"做過Java的開發項目的同學也許查看某個webServer或者Rpc的Server時候會發現經常pid有兩個(如下圖):其實一個是父進程,另一個就是子進程,更新專業的說一個是LWP也就是輕量級的線程。:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/77/771f07277f07cc7c9a3cc6d4d7854a19.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 用一張圖來描述cpu的線程調度和進程的關係:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/fb/fb84851bf2c92d384b83a98998df122c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Java的線程調度中,我們可以發現其實即便Jvm開啓的線程,但是本質上最終還是綁定內核態的一個線程,所以對於一個大型程序,我們可以開闢的線程數量至少等於運行機器的cpu內核數量。java程序裏我們可以通過下面的一行代碼得到這個數量:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"Runtime.getRuntime().availableProcessors();","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以最小線程數量即時cpu內核數量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果所有的任務都是計算密集型的,這個最小線程數量就是我們需要的線程數。開闢更多的線程只會影響程序的性能,因爲線程之間的切換工作,會消耗額外的資源。如果任務是IO密集型的任務,我們可以開闢更多的線程執行任務。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當一個任務執行IO操作的時候,線程將會被阻塞,處理器立刻會切換到另外一個合適的線程去執行。如果我們只擁有與內核數量一樣多的線程,即使我們有任務要執行,他們也不能執行,因爲處理器沒有可以用來調度的線程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果線程有50%的時間被阻塞,線程的數量就應該是內核數量的2倍。如果更少的比例被阻塞,那麼它們就是計算密集型的,則需要開闢較少的線程。如果有更多的時間被阻塞,那麼就是IO密集型的程序,則可以開闢更多的線程。於是我們可以得到下面的線程數量計算公式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 線程數量 = 內核數量 / (1 - 阻塞率)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Golang調度模型","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過Java的線程調度,我們可以發現其實線程分爲用戶態和內核態的,在Java中只不過是一個用戶態線程通過native的線程映射到一個LWP線程(前文提到的輕量級線程,作用域通過內核態支持用戶態線程),最終綁定了一個內核態的線程。而Golang恰巧利用了這一點,就是自己組織調度用戶態的線程,因爲CPU最終只關注內核態的線程,於是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"協程就誕生了","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"在轉Golang之前,一直停其他開發同學說:Golang比較牛逼就是協程,一種跟輕量級的東西,比線程佔用的內存更小,切換的時候,說消耗的CPU更小。","attrs":{}},{"type":"text","text":"眼見爲實,耳聽爲虛。 眼見不一定爲實,耳聽不一定爲虛。本質上協程是用戶態的線程的一種調度管理,簡單點來說就是:Golang 通過goruntime(協程)這種東西,在內部維持一個固定線程數的線程池,進行合理的調度,使得每一個線程上執行更多的工作來降低操作系統和硬件的負載。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"腦洞開始了","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"N:1模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然可以自行的管理用戶線程,那麼我們可以開啓N個goruntime然後通過調度器來最終綁定一個內核態的線程。這樣協程在用戶態線程即完成切換,不會陷入到內核態,這種切換非常的輕量快速。具體如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4a/4a4f683bb09687fcb0bf8c55ed08a8e8.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況很快發現了缺點,所以的線程都綁定一個線程上,如果某一個協程阻塞了,那麼其他的整個線程就阻塞了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"N:M模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於上述的情況,我們可以考慮讓一組線程綁定到一組線程,這樣的話,就不會因爲一個協程阻塞而導致其他的協程阻塞。 具體如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/aa/aaaee046239c07c536c651907df1151c.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在協程的調取器問題依舊存在: ","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"中創建、銷燬、調度goruntime都需要每個線程獲取鎖,這就形成了激烈的鎖競爭。","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"線程轉移goruntime會造成延遲和額外的系統負載。+ 系統調用(CPU在線程之間的切換)導致頻繁的線程阻塞和取消阻塞操作增加了系統開銷。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"GMP模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於上述問題,Golang設計了一套新的協程調度模式:GMP模式,即在原來M(thread線程)和G(goruntime)中引入了P(processor);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Processor,它包含了運行goroutine的資源,如果線程想運行goroutine,必須先獲取P,P中還包含了可運行的G隊列。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"GMP流程圖:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/80db0402cd911927ac661dfc09bbf1c9.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流程文字描述: ","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 go func()來創建一個goroutine; ","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有兩個存儲G的隊列,一個是局部調度器P的本地隊列、一個是全局G隊列。新創建的G會先保存在P的本地隊列中,如果P的本地隊列已經滿了就會保存在全局的隊列中;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"G只能運行在M中,一個M必須持有一個P,M與P是1:1的關係。M會從P的本地隊列彈出一個可執行狀態的G來執行,如果P的本地隊列爲空,就會想其他的MP組合偷取一個可執行的G來執行;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個M調度G執行的過程是一個循環機制;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當M執行某一個G時候如果發生了syscall或則其餘阻塞操作,M會阻塞,如果當前有一些G在執行,runtime會把這個線程M從P中摘除(detach),然後再創建一個新的操作系統的線程(如果有空閒的線程可用就複用空閒線程)來服務於這個P;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當M系統調用結束時候,這個G會嘗試獲取一個空閒的P執行,並放入到這個P的本地隊列。如果獲取不到P,那麼這個線程M變成休眠狀態, 加入到空閒線程中,然後這個G會被放入全局隊列中。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"上代碼,用實踐證明","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go code:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package main\n\nimport (\n \"fmt\"\n \"os\"\n \"runtime\"\n \"runtime/trace\"\n \"time\"\n)\n\nfunc main() {\n f, err := os.Create(\"trace.out\")\n defer f.Close()\n if err != nil {\n fmt.Println(err)\n }\n err = trace.Start(f)\n defer trace.Stop()\n if err != nil {\n fmt.Println(err)\n }\n for i := 0; i < 5; i++ {\n time.Sleep(time.Second)\n fmt.Println(\"GMP\")\n }\n fmt.Println(\"cpus:\", runtime.NumCPU())\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們借Go tool 的trace功能來看看程序的結果:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a458c9ed88fc54ae6eb863438dabc446.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9c/9c69d86f2857e906a0beaa2ad1009290.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/46/46c141f5dd838b2a5cb8411659e40d47.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"我們再通過debug清晰的分析,這裏不禁想起了看Java的GClog日誌:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/58/58ce2b4919f6166e51af9448d79e2395.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們挑上述圖中關鍵點來簡單說明一下: ","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"gomaxprocs: gomaxprocs=12默認是cpu的核數,這裏測試的是CPU12核,通過GOMAXPROCS來設置;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"idleprocs: 處於idle狀態的P的數量;通過gomaxprocs和idleprocs的差值,我們就可知道執行go代碼的P的數量;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"threads: os threads/M的數量,包含scheduler使用的m數量,加上runtime自用的類似sysmon這樣的thread的數量;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"spinningthreads: 處於自旋狀態的os thread數量;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"idlethread: 處於idle狀態的os thread的數量;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"runqueue=0: 即對應了前面的Scheduler全局隊列中G的數量;","attrs":{}}]}],"attrs":{}},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[0 0 0 0 0 0 0 0]: 貌似是本地local queue中的G的數組長度,但是理論上都是0,這裏自己也太清晰。","attrs":{}}]}],"attrs":{}}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從linux的系統調度,借鑑了Java的線程調度,最終可以看到Go裏GMP的模式巧妙藉助了用戶態的線程,讓Goroutine實現了佔用更小的內存,降低了CPU的切換次(注意並沒有消除了上下文切換,畢竟最終還是依賴內核線程調度)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章