在操做系統中,執行體是個抽象的概念。與之對應的實體有進程、線程以及協程(coroutine)。協程也叫輕量級的線程,與傳統的進程和線程相比,協程的最大特色是 "輕"!能夠輕鬆建立上百萬個協程而不會致使系統資源衰竭。
多數編程語言在語法層面並不直接支持協程,而是經過庫的方式支持。可是用庫的方式支持的功能每每不是很完整,好比僅僅提供輕量級線程的建立、銷燬和切換等能力。若是在這樣的協程中調用一個同步 IO 操做,好比網絡通訊、本地文件讀寫,都會阻塞其餘的併發執行的協程,從而沒法達到輕量級線程自己指望達到的目標。html
Golang 在語言級別支持協程,稱之爲 goroutine。Golang 標準庫提供的全部系統調用操做(包括全部的同步 IO 操做),都會出讓 CPU 給其餘 goroutine。這讓 goroutine 的切換管理不依賴於系統的線程和進程,也不依賴於 CPU 的核心數量,而是交給 Golang 的運行時統一調度。算法
goroutine 是 Golang 中併發設計的核心,更多關於併發的概念,請參考《Golang 入門 : 理解併發與並行》。 本文接下來的部分着重經過 demo 介紹 goroutine 的用法。編程
要在一個協程中運行函數,直接在調用函數時添加關鍵字 go 就能夠了:網絡
package main import ( "time" "fmt" ) func say(s string) { for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("hello world") time.Sleep(1000 * time.Millisecond) fmt.Println("over!") }
執行上面的代碼,輸出結果爲:併發
hello world
hello world
hello world
over!
至於爲何要在 main 函數中調用 Sleep,如何用優雅的方式代替 Sleep,請參考《Golang 入門 : 等待 goroutine 完成任務》一文。編程語言
讓咱們經過下面的 demo 來理解 goroutine 的生命週期:函數
package main import ( "runtime" "sync" "fmt" ) func main() { // 分配一個邏輯處理器給調度器使用 runtime.GOMAXPROCS(1) // wg用來等待程序完成 // 計數加2,表示要等待兩個goroutine var wg sync.WaitGroup wg.Add(2) fmt.Println("Start Goroutines") // 聲明一個匿名函數,並建立一個goroutine go func(){ // 在函數退出時調用Done來通知main函數工做已經完成 defer wg.Done() // 顯示字母表3次 for count := 0; count< 3; count++{ for char := 'a'; char< 'a'+26; char++{ fmt.Printf("%c ", char) } fmt.Println() } }() // 聲明一個匿名函數,並建立一個goroutine go func(){ // 在函數退出時調用Done來通知main函數工做已經完成 defer wg.Done() // 顯示字母表3次 for count := 0; count< 3; count++{ for char := 'A'; char< 'A'+26; char++{ fmt.Printf("%c ", char) } fmt.Println() } }() // 等待goroutine結束 fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("Terminating Program") }
在 demo 的起始部分,經過調用 runtime 包中的 GOMAXPROCS 函數,把可使用的邏輯處理器的數量設置爲 1。
接下來經過 goroutine 執行的兩個匿名函數分別輸出三遍小寫字母和三遍大寫字母。運行上面代碼,輸出的結果以下:spa
Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program
第一個 goroutine 完成全部任務的時間過短了,以致於在調度器切換到第二個 goroutine 以前,就完成了全部任務。這也是爲何會看到先輸出了全部的大寫字母,以後才輸出小寫字母。咱們建立的兩個 goroutine 一個接一個地併發運行,獨立完成顯示字母表的任務。
由於 goroutine 以非阻塞的方式執行,它們會隨着程序(主線程)的結束而消亡,因此咱們在 main 函數中使用 WaitGroup 來等待兩個 goroutine 完成他們的工做,更多 WaitGroup 相關的信息,請參考《Golang 入門 : 等待 goroutine 完成任務》一文。操作系統
基於調度器的內部算法,一個正運行的 goroutine 在工做結束前,能夠被中止並從新調度。調度器這樣作的目的是防止某個 goroutine 長時間佔用邏輯處理器。當 goroutine 佔用時間過長時,調度器會中止當前正運行的 goroutine,並給其餘可運行的 goroutine 運行的機會。
讓咱們經過下圖來理解這一場景(下圖來自互聯網):線程
讓咱們經過一個運行時間長些的任務來觀察該行爲,運行下面的 代碼:
package main import ( "runtime" "sync" "fmt" ) func main() { // wg用來等待程序完成 var wg sync.WaitGroup // 分配一個邏輯處理器給調度器使用 runtime.GOMAXPROCS(1) // 計數加2,表示要等待兩個goroutine wg.Add(2) // 建立兩個goroutine fmt.Println("Create Goroutines") go printPrime("A", &wg) go printPrime("B", &wg) // 等待goroutine結束 fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("Terminating Program") } // printPrime 顯示5000之內的素數值 func printPrime(prefix string, wg *sync.WaitGroup){ // 在函數退出時調用Done來通知main函數工做已經完成 defer wg.Done() next: for outer := 2; outer < 5000; outer++ { for inner := 2; inner < outer; inner++ { if outer % inner == 0 { continue next } } fmt.Printf("%s:%d\n", prefix, outer) } fmt.Println("Completed", prefix) }
代碼中運行了兩個 goroutine,分別打印 1-5000 內的素數,輸出的結果比較長,精簡以下:
Create Goroutines Waiting To Finish B:2 B:3 ... B:3851 A:2 ** 切換 goroutine A:3 ... A:4297 B:3853 ** 切換 goroutine ... B:4999 Completed B A:4327 ** 切換 goroutine ... A:4999 Completed A Terminating Program
上面的輸出說明:goroutine B 先執行,而後切換到 goroutine A,再切換到 goroutine B 運行至任務結束,最後又切換到 goroutine A,運行至任務結束。注意,每次運行這個程序,調度器切換的時間點都會稍有不一樣。
前面的兩個示例,經過設置 runtime.GOMAXPROCS(1),強制讓 goroutine 在一個邏輯處理器上併發執行。用一樣的方式,咱們能夠設置邏輯處理器的個數等於物理處理器的個數,從而讓 goroutine 並行執行(物理處理器的個數得大於 1)。
下面的代碼可讓邏輯處理器的個數等於物理處理器的個數:
runtime.GOMAXPROCS(runtime.NumCPU())
其中的函數 NumCPU 返回可使用的物理處理器的數量。所以,調用 GOMAXPROCS 函數就爲每一個可用的物理處理器建立一個邏輯處理器。注意,從 Golang 1.5 開始,GOMAXPROCS 的默認值已經等於可使用的物理處理器的數量了。
修改上面輸出素數的程序:
runtime.GOMAXPROCS(2)
由於咱們只建立了兩個 goroutine,因此邏輯處理器的數量設置爲 2 就能夠了,從新運行該程序,看看是否是 A 和 B 的輸出混合在一塊兒了:
... B:1741 B:1747 A:241 A:251 B:1753 A:257 A:263 A:269 A:271 A:277 B:1759 A:281 ...
除了這個 demo 程序,在真實場景中這種並行的方式會帶來不少數據同步的問題。接下來咱們將介紹如何來解決數據的同步問題。
參考:
《Go語言實戰》