[TOC]
## CSP 模型?
CSP 模型是“以通信的方式來共享內存”,不同于傳統的多線程通過共享內存來通信。用于描述兩個獨立的并發實體通過共享的通訊 channel (管道)進行通信的并發模型。
## GMP
### GMP解釋
G:Goroutine,實際上我們每次調用`go func`就是生成了一個 G。
P:Processor,處理器,一般 P 的數量就是處理器的核數,可以通過`GOMAXPROCS`進行修改。
M:Machine,系統線程。
在 GPM 模型,有一個全局隊列(Global Queue):存放等待運行的 G,還有一個 P 的本地隊列:也是存放等待運行的 G,但數量有限,不超過 256 個。
### GMP模型
在Go中,**線程是運行goroutine的實體,調度器的功能是把可運行的goroutine分配到工作線程上**

1. **全局隊列**(Global Queue):存放等待運行的G。
2. **P的本地隊列**:同全局隊列類似,存放的也是等待運行的G,存的數量有限,不超過256個。新建G'時,G'優先加入到P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
3. **P列表**:所有的P都在程序啟動時創建,并保存在數組中,最多有`GOMAXPROCS`(可配置)個。
4. **M**:線程想運行任務就得獲取P,從P的本地隊列獲取G,P隊列為空時,M也會嘗試從全局隊列**拿**一批G放到P的本地隊列,或從其他P的本地隊列**偷**一半放到自己P的本地隊列。M運行G,G執行之后,M會從P獲取下一個G,不斷重復下去。
**Goroutine調度器和OS調度器是通過M結合起來的,每個M都代表了1個內核線程,OS調度器負責把內核線程分配到CPU的核上執行**。
> #### 有關P和M的個數問題
1、P的數量:
* 由啟動時環境變量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`決定。這意味著在程序執行的任意時刻都只有`$GOMAXPROCS`個goroutine在同時運行。
2、M的數量:
* go語言本身的限制:go程序啟動時,會設置M的最大數量,默認10000.但是內核很難支持這么多的線程數,所以這個限制可以忽略。
* runtime/debug中的SetMaxThreads函數,設置M的最大數量
* 一個M阻塞了,會創建新的M。
M與P的數量沒有絕對關系,一個M阻塞,P就會去創建或者切換另一個M,所以,即使P的默認數量是1,也有可能會創建很多個M出來。
> #### P和M何時會被創建
1、P何時創建:在確定了P的最大數量n后,運行時系統會根據這個數量創建n個P。
2、M何時創建:沒有足夠的M來關聯P并運行其中的可運行的G。比如所有的M此時都阻塞住了,而P中還有很多就緒任務,就會去尋找空閑的M,而沒有空閑的,就會去創建新的M。
### 調度器的設計策略
**復用線程**:避免頻繁的創建、銷毀線程,而是對線程的復用。
1)work stealing機制
? 當本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程。
2)hand off機制
? 當本線程因為G進行系統調用阻塞時,線程釋放綁定的P,把P轉移給其他空閑的線程執行。
**利用并行**:`GOMAXPROCS`設置P的數量,最多有`GOMAXPROCS`個線程分布在多個CPU上同時運行。`GOMAXPROCS`也限制了并發的程度,比如`GOMAXPROCS = 核數/2`,則最多利用了一半的CPU核進行并行。
**搶占**:在coroutine中要等待一個協程主動讓出CPU才執行下一個協程,在Go中,一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死,這就是goroutine不同于coroutine的一個地方。
**全局G隊列**:在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。
### GMP調度流程

1、 GPM 的調度流程從 go func()開始創建一個 goroutine,新建的G優先放入P的本地隊列保存待執行的 goroutine(流程 2),當 M 綁定的 P 的的局部隊列已經滿了之后就會把 goroutine 放到全局隊列(流 程 2-1)
2、每個 P 和一個 M 綁定,M 是真正的執行 P 中 goroutine 的實體(流程 3), M 從綁定的 P 中的局部隊列獲取 G 來執行
3、當 M 綁定的 P 的局部隊列為空時,M 會從全局隊列獲取到本地隊列來執行 G (流程 3.1),當從全局隊列中沒有獲取到可執行的 G 時候,M 會從其他 P 的局部隊列中偷取 G 來執行(流程 3.2),這種從其他 P 偷的方式稱為 work stealing
4、一個M調度G執行的過程是一個循環機制
5、 當 G 因系統調用(syscall)阻塞時會阻塞 M,此時 P 會和 M 解綁即 hand off,并尋找新的空閑的 M,若沒有空閑的 M 就會新建一個 M(流程 5.1)
6、當 G 因 channel 或者 network I/O 阻塞時,不會阻塞 M,M 會尋找其他的 G;當阻塞的 G 恢復后會重新進入 runnable 進入 P 隊列等待執 行(流程 5.3)
7、 當M系統調用結束時候,這個G會嘗試獲取一個空閑的P執行,并放入到這個P的本地隊列。如果獲取不到P,則將G放入全局隊列,等待被其他的P調度。然后M將進入緩存池睡眠。
### GMP 中 work stealing 機制
獲取 P 本地隊列,當從綁定 P 本地 runq 上找不到可執行的 g,嘗試從全局鏈 表中拿,再拿不到從 netpoll 和事件池里拿,最后會從別的 P 里偷任務。P 此時去喚醒一個 M。P 繼續執行其它的程序。M 尋找是否有空閑的 P,如果有則 將該 G 對象移動到它本身。接下來 M 執行一個調度循環(調用 G 對象->執行-> 清理線程→繼續找新的 Goroutine 執行)
### hand off 機制
當本線程 M 因為 G 進行的系統調用阻塞時,線程釋放綁定的 P,把 P 轉移給其 他空閑的 M 執行。
細節:當發生上線文切換時,需要對執行現場進行保護,以便下次被調度執行 時進行現場恢復。Go 調度器 M 的棧保存在 G 對象上,只需要將 M 所需要的寄存 器(SP、PC 等)保存到 G 對象上就可以實現現場保護。當這些寄存器數據被保 護起來,就隨時可以做上下文切換了,在中斷之前把現場保存起來。如果此時 G 任務還沒有執行完,M 可以將任務重新丟到 P 的任務隊列,等待下一次被調度 執行。當再次被調度執行時,M 通過訪問 G 的 vdsoSP、vdsoPC 寄存器進行現場 恢復(從上次中斷位置繼續執行)。
### **搶占式調度是如何搶占的**
#### 協作式的搶占式調度
1. 如果 sysmon 監控線程發現有個協程 A 執行之間太長了(或者 gc 場景,或者 stw 場景),那么會友好的在這個 A 協程的某個字段設置一個搶占標記 ;
2. 協程 A 在 call 一個函數的時候,會復用到擴容棧(morestack)的部分邏輯,檢查到搶占標記之后,讓出 cpu,切到調度主協程里;
之所以說 v1.2 的搶占式調用是臨時的優化方案,是因為這種搶占式調度是基于協作的。在一些的邊緣場景下,協程還是在會獨自占用整個線程無法讓出。
從上面的流程中,你應該可以注意到,A 調度權被搶占有個前提:A 必須主動 call 函數,這樣才能有走到 morestack 的機會。
等著主動讓出,會出現
#### 基于信號的搶占式調度
不管是否愿意讓出cpu只要運行超過20ms,就會發現號強行搶占cpu
>極客時間的解釋
在任何情況下,Go 運行時并行執行(注意,不是并發)的 goroutines 數量是 小于等于 P 的數量的。為了提高系統的性能,P 的數量肯定不是越小越好,所 以官方默認值就是 CPU 的核心數,設置的過小的話,如果一個持有 P 的 M, 由于 P 當前執行的 G 調用了 syscall 而導致 M 被阻塞,那么此時關鍵點: GO 的調度器是遲鈍的,它很可能什么都沒做,直到 M 阻塞了相當長時間以 后,才會發現有一個 P/M 被 syscall 阻塞了。然后,才會用空閑的 M 來強這 個 P。通過 sysmon 監控實現的搶占式調度,最快在 20us,最慢在 10-20ms 才 會發現有一個 M 持有 P 并阻塞了。操作系統在 1ms 內可以完成很多次線程調 度(一般情況 1ms 可以完成幾十次線程調度),Go 發起 IO/syscall 的時候執 行該 G 的 M 會阻塞然后被 OS 調度走,P 什么也不干,sysmon 最慢要 10-20ms 才能發現這個阻塞,說不定那時候阻塞已經結束了,這樣寶貴的 P 資源就這么 被阻塞的 M 浪費了。
```
runtime.GOMAXPROCS(1)
fmt.Println("The program starts ...")
go func() {
for {
}
}()
time.Sleep(time.Second)
fmt.Println("I got scheduled!")
```
這段代碼在1.14之前就會死循環,在之后就會直接跳出
**基于信號的搶占式調度,搶占也只會在垃圾回收掃描任務時觸發**
### GMP 調度過程中存在哪些阻塞
1. I/O,select
2. block on syscall
3. channel
4. 等待鎖
5. runtime.Gosched()
### Sysmon 有什么作用
Sysmon 也叫監控線程,變動的周期性檢查,好處
* 釋放閑置超過 5 分鐘的 span 物理內存;
* 如果超過 2 分鐘沒有垃圾回收,強制執行;
* 將長時間未處理的 netpoll 添加到全局隊列;
* 向長時間運行的 G 任務發出搶占調度(超過 10ms 的 g,會進行 retake);
* 收回因 syscall 長時間阻塞的 P;
### G-M-P的數量關系
* M:有限制,默認數量限制是 10000,可調整。(debug.SetMaxThreads 設置)
* G:沒限制,但受內存影響。
~~~
假設一個 Goroutine 創建需要 4k:
4k * 80,000 = 320,000k ≈ 0.3G內存
4k * 1,000,000 = 4,000,000k ≈ 4G內存
以此就可以相對計算出來一臺單機在通俗情況下,所能夠創建 Goroutine 的大概數量級別。
注:Goroutine 創建所需申請的 2-4k 是需要連續的內存塊。
~~~
* P:受本機的核數影響,可大可小,不影響 G 的數量創建。(**`GOMAXPROCS`**)
### 調度器的生命周期

特殊的M0和G0
**M0**
`M0`是啟動程序后的編號為0的主線程,這個M對應的實例會在全局變量runtime.m0中,不需要在heap上分配,M0負責執行初始化操作和啟動第一個G, 在之后M0就和其他的M一樣了。
**G0**
`G0`是每次啟動一個M都會第一個創建的gourtine,G0僅用于負責調度的G,G0不指向任何可執行的函數, 每個M都會有一個自己的G0。在調度或系統調用時會使用G0的棧空間, 全局變量的G0是M0的G0。
- Go準備工作
- 依賴管理
- Go基礎
- 1、變量和常量
- 2、基本數據類型
- 3、運算符
- 4、流程控制
- 5、數組
- 數組聲明和初始化
- 遍歷
- 數組是值類型
- 6、切片
- 定義
- slice其他內容
- 7、map
- 8、函數
- 函數基礎
- 函數進階
- 9、指針
- 10、結構體
- 類型別名和自定義類型
- 結構體
- 11、接口
- 12、反射
- 13、并發
- 14、網絡編程
- 15、單元測試
- Go常用庫/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go優化
- Go問題排查
- Go框架
- 基礎知識點的思考
- 面試題
- 八股文
- 操作系統
- 整理一份資料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小點
- 樹
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面試題
- 基礎
- Map
- Chan
- GC
- GMP
- 并發
- 內存
- 算法
- docker