[TOC]
goroutine 是 Golang 語言中的輕量級線程實現,由 Golang 運行時(Runtime)管理。
Goroutine 是 Go 語言支持并發的核心,在一個Go程序中同時創建成百上千個goroutine是非常普遍的,一個goroutine會以一個很小的棧開始其生命周期,一般只需要2KB。區別于操作系統線程由系統內核進行調度, goroutine 是由Go運行時(runtime)負責調度。例如Go運行時會智能地將 m個goroutine 合理地分配給n個操作系統線程,實現類似m:n的調度機制,不再需要Go開發者自行在代碼層面維護一個線程池。
Goroutine 是 Go 程序中最基本的并發執行單元。每一個 Go 程序都至少包含一個 goroutine -> main goroutine,當 Go 程序啟動時它會自動創建。
在Go語言編程中你不需要去自己寫進程、線程、協程,你的技能包里只有一個技能——goroutine,當你需要讓某個任務并發執行的時候,你只需要把這個任務包裝成一個函數,開啟一個 goroutine 去執行這個函數就可以了,就是這么簡單粗暴。
## 啟動單個goroutine
```go
func demo1() {
fmt.Println("hello demo1 ~")
}
func main() {
demo1()
fmt.Println("hello world ~")
}
// 運行結果
// hello demo1 ~
// hello world ~
```
代碼中 demo1 函數和其后面的打印語句是串行的。

接下來我們在調用 demo1 函數前面加上關鍵字go,也就是啟動一個 goroutine 去執行 demo1 這個函數。
```go
func demo1() {
fmt.Println("hello demo1 ~")
}
func main() {
go demo1()
fmt.Println("hello world ~")
}
// 運行結果
// hello world ~
```
這個運行結果不是預想的結果,為什么是出現這個問題呢?
其實在 Go 程序啟動時,Go 程序就會為 main 函數創建一個默認的 goroutine 。在上面的代碼中我們在 main 函數中使用 go 關鍵字創建了另外一個 goroutine 去執行 demo1 函數,而此時 `main goroutine` 還在繼續往下執行,我們的程序中此時存在兩個并發執行的 goroutine。當 main 函數結束時整個程序也就結束了,同時 `main goroutine` 也結束了,所有由 `main goroutine` 創建的 goroutine 也會一同退出(例如 `demo1 goroutine` )。
所以我們要想辦法讓 main 函數執行慢一點,就讓 main 函數沉睡個 `500ms` 等待 `demo1 goroutrine` 結束。
```go
func demo1() {
fmt.Println("hello demo1 ~")
}
func main() {
go demo1()
fmt.Println("hello world ~")
time.Sleep(time.Millisecond * 500)
}
// 運行結果
// hello world ~
// hello demo1 ~
```
為什么會先打印 `hello world ~` ,然后再打印 `hello demo1 ~` 。與代碼的運行順序不同呢?
這是因為在程序中創建 goroutine 執行函數需要一定的開銷,而與此同時 main 函數所在的 goroutine 是繼續執行的。

程序中有 `time.Sleep` 多少才合適呢?時間短了有些協程未執行完成,長了返回結果慢了。這里應用一個等待鎖。
```go
var sw sync.WaitGroup
func demo1() {
// goroutine結束就登記-1
defer sw.Done()
fmt.Println("hello demo1 ~")
}
func main() {
// 啟動一個goroutine就登記+1
sw.Add(1)
go demo1()
fmt.Println("hello world ~")
// 等待所有登記的goroutine都結束
sw.Wait()
}
```
## 啟動多個goroutine
```go
func demo2(i int) {
// goroutine結束就登記-1
defer sw.Done()
fmt.Printf("value is %d\n", i)
}
func main() {
// 使用for循環創建9個協程
for i := 1; i < 10; i++ {
// 啟動一個goroutine就登記+1
sw.Add(1)
go demo2(i)
}
// 打印 hello world ~ 字符串
fmt.Println("hello world ~")
// 等待所有登記的goroutine都結束
sw.Wait()
}
// 運行結果
// hello world ~
// value is 1
// value is 6
// value is 9
// value is 7
// value is 8
// value is 2
// value is 3
// value is 4
// value is 5
```
>[info] 注意:創建多個goroutine執行任務,不是先創建goroutine一定先執行完成的。從上面結果可知,每次執行完的順序都是不一樣的。
## 動態棧
操作系統的線程一般都有固定的棧內存(通常為2MB),而 Go 語言中的 goroutine 非常輕量級,一個 goroutine 的初始棧空間很小(一般為2KB),所以在 Go 語言中一次創建數萬個 goroutine 也是可能的。并且 goroutine 的棧不是固定的,可以根據需要動態地增大或縮小, Go 的 runtime 會自動為 goroutine 分配合適的棧空間。
## goroutine調度
操作系統內核在調度時會掛起當前正在執行的線程并將寄存器中的內容保存到內存中,然后選出接下來要執行的線程并從內存中恢復該線程的寄存器信息,然后恢復執行該線程的現場并開始執行線程。從一個線程切換到另一個線程需要完整的上下文切換。因為可能需要多次內存訪問,索引這個切換上下文的操作開銷較大,會增加運行的cpu周期。
區別于操作系統內核調度操作系統線程,goroutine 的調度是Go語言運行時(runtime)層面的實現,是完全由 Go 語言本身實現的一套調度系統——go scheduler。它的作用是按照一定的規則將所有的 goroutine 調度到操作系統線程上執行。
在經歷數個版本的迭代之后,目前 Go 語言的調度器采用的是 GPM 調度模型。

圖片的字母的說明
- G:表示 goroutine,每執行一次go f()就創建一個 G,包含要執行的函數和上下文信息。
- 全局隊列(Global Queue):存放等待運行的 G。
- P:表示 goroutine 執行所需的資源,最多有 GOMAXPROCS 個。
- P 的本地隊列:同全局隊列類似,存放的也是等待運行的G,存的數量有限,不超過256個。新建 G 時,G 優先加入到 P 的本地隊列,如果本地隊列滿了會批量移動部分 G 到全局隊列。
- M:線程想運行任務就得獲取 P,從 P 的本地隊列獲取 G,當 P 的本地隊列為空時,M 也會嘗試從全局隊列或其他 P 的本地隊列獲取 G。M 運行 G,G 執行之后,M 會從 P 獲取下一個 G,不斷重復下去。
- Goroutine 調度器和操作系統調度器是通過 M 結合起來的,每個 M 都代表了1個內核線程,操作系統調度器負責把內核線程分配到 CPU 的核上執行。
單從線程調度講,Go語言相比起其他語言的優勢在于OS線程是由OS內核來調度的, goroutine 則是由Go運行時(runtime)自己的調度器調度的,完全是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護著一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身 goroutine 的超輕量級,以上種種特性保證了 goroutine 調度方面的性能。
## GOMAXPROCS
Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個 OS 線程來同時執行 Go 代碼。默認值是機器上的 CPU 核心數。例如在一個 8 核心的機器上,GOMAXPROCS 默認為 8。Go語言中可以通過runtime.GOMAXPROCS函數設置當前程序并發時占用的 CPU邏輯核心數。(Go1.5版本之前,默認使用的是單核心執行。Go1.5 版本之后,默認使用全部的CPU 邏輯核心數。)
- Golang簡介
- 開發環境
- Golang安裝
- 編輯器及快捷鍵
- vscode插件
- 第一個程序
- 基礎數據類型
- 變量及匿名變量
- 常量與iota
- 整型與浮點型
- 復數與布爾值
- 字符串
- 運算符
- 算術運算符
- 關系運算符
- 邏輯運算符
- 位運算符
- 賦值運算符
- 流程控制語句
- 獲取用戶輸入
- if分支語句
- for循環語句
- switch語句
- break_continue_goto語法
- 高階數據類型
- pointer指針
- array數組
- slice切片
- slice切片擴展
- map映射
- 函數
- 函數定義和調用
- 函數參數
- 函數返回值
- 作用域
- 函數形參傳遞
- 匿名函數
- 高階函數
- 閉包
- defer語句
- 內置函數
- fmt
- strconv
- strings
- time
- os
- io
- 文件操作
- 編碼
- 字符與字節
- 字符串
- 讀寫文件
- 結構體
- 類型別名和自定義類型
- 結構體聲明
- 結構體實例化
- 模擬構造函數
- 方法接收器
- 匿名字段
- 嵌套與繼承
- 序列化
- 接口
- 接口類型
- 值接收者和指針接收者
- 類型與接口對應關系
- 空接口
- 接口值
- 類型斷言
- 并發編程
- 基本概念
- goroutine
- channel
- select
- 并發安全
- 練習題
- 第三方庫
- Survey
- cobra