## 14.1.1 什么是協程
一個應用程序是運行在機器上的一個進程;進程是一個運行在自己內存地址空間里的獨立執行體。一個進程由一個或多個操作系統線程組成,這些線程其實是共享同一個內存地址空間的一起工作的執行體。幾乎所有'正式'的程序都是多線程的,以便讓用戶或計算機不必等待,或者能夠同時服務多個請求(如 Web 服務器),或增加性能和吞吐量(例如,通過對不同的數據集并行執行代碼)。一個并發程序可以在一個處理器或者內核上使用多個線程來執行任務,但是只有同一個程序在某個時間點同時運行在多核或者多處理器上才是真正的并行。
并行是一種通過使用多處理器以提高速度的能力。所以并發程序可以是并行的,也可以不是。
公認的,使用多線程的應用難以做到準確,最主要的問題是內存中的數據共享,它們會被多線程以無法預知的方式進行操作,導致一些無法重現或者隨機的結果(稱作?`競態`)。
不要使用全局變量或者共享內存,它們會給你的代碼在并發運算的時候帶來危險。
解決之道在于同步不同的線程,對數據加鎖,這樣同時就只有一個線程可以變更數據。在 Go 的標準庫?`sync`?中有一些工具用來在低級別的代碼中實現加鎖;我們在第?[9.3](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/9.3.md)?節中討論過這個問題。不過過去的軟件開發經驗告訴我們這會帶來更高的復雜度,更容易使代碼出錯以及更低的性能,所以這個經典的方法明顯不再適合現代多核/多處理器編程:`thread-per-connection`?模型不夠有效。
Go 更傾向于其他的方式,在諸多比較合適的范式中,有個被稱作?`Communicating Sequential Processes(順序通信處理)`(CSP, C. Hoare 發明的)還有一個叫做?`message passing-model(消息傳遞)`(已經運用在了其他語言中,比如 Erlang)。
在 Go 中,應用程序并發處理的部分被稱作?`goroutines(協程)`,它可以進行更有效的并發運算。在協程和操作系統線程之間并無一對一的關系:協程是根據一個或多個線程的可用性,映射(多路復用,執行于)在他們之上的;協程調度器在 Go 運行時很好的完成了這個工作。
協程工作在相同的地址空間中,所以共享內存的方式一定是同步的;這個可以使用?`sync`?包來實現(參見第?[9.3](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/9.3.md)?節),不過我們很不鼓勵這樣做:Go 使用?`channels`?來同步協程(可以參見第?[14.2](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.2.md)?節等章節)
當系統調用(比如等待 I/O)阻塞協程時,其他協程會繼續在其他線程上工作。協程的設計隱藏了許多線程創建和管理方面的復雜工作。
協程是輕量的,比線程更輕。它們痕跡非常不明顯(使用少量的內存和資源):使用 4K 的棧內存就可以在堆中創建它們。因為創建非常廉價,必要的時候可以輕松創建并運行大量的協程(在同一個地址空間中 100,000 個連續的協程)。并且它們對棧進行了分割,從而動態的增加(或縮減)內存的使用;棧的管理是自動的,但不是由垃圾回收器管理的,而是在協程退出后自動釋放。
協程可以運行在多個操作系統線程之間,也可以運行在線程之內,讓你可以很小的內存占用就可以處理大量的任務。由于操作系統線程上的協程時間片,你可以使用少量的操作系統線程就能擁有任意多個提供服務的協程,而且 Go 運行時可以聰明的意識到哪些協程被阻塞了,暫時擱置它們并處理其他協程。
存在兩種并發方式:確定性的(明確定義排序)和非確定性的(加鎖/互斥從而未定義排序)。Go 的協程和通道理所當然的支持確定性的并發方式(例如通道具有一個 sender 和一個 receiver)。我們會在第?[14.7](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.7.md)?節中使用一個常見的算法問題(工人問題)來對比兩種處理方式。
協程是通過使用關鍵字?`go`?調用(執行)一個函數或者方法來實現的(也可以是匿名或者 lambda 函數)。這樣會在當前的計算過程中開始一個同時進行的函數,在相同的地址空間中并且分配了獨立的棧,比如:`go sum(bigArray)`,在后臺計算總和。
協程的棧會根據需要進行伸縮,不出現棧溢出;開發者不需要關心棧的大小。當協程結束的時候,它會靜默退出:用來啟動這個協程的函數不會得到任何的返回值。
任何 Go 程序都必須有的?`main()`?函數也可以看做是一個協程,盡管它并沒有通過?`go`?來啟動。協程可以在程序初始化的過程中運行(在?`init()`?函數中)。
在一個協程中,比如它需要進行非常密集的運算,你可以在運算循環中周期的使用?`runtime.Gosched()`:這會讓出處理器,允許運行其他協程;它并不會使當前協程掛起,所以它會自動恢復執行。使用?`Gosched()`?可以使計算均勻分布,使通信不至于遲遲得不到響應。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md#1412-并發和并行的差異)14.1.2 并發和并行的差異
Go 的并發原語提供了良好的并發設計基礎:表達程序結構以便表示獨立地執行的動作;所以Go的的重點不在于并行的首要位置:并發程序可能是并行的,也可能不是。并行是一種通過使用多處理器以提高速度的能力。但往往是,一個設計良好的并發程序在并行方面的表現也非常出色。
在當前的運行時(2012 年一月)實現中,Go 默認沒有并行指令,只有一個獨立的核心或處理器被專門用于 Go 程序,不論它啟動了多少個協程;所以這些協程是并發運行的,但他們不是并行運行的:同一時間只有一個協程會處在運行狀態。
這個情況在以后可能會發生改變,不過屆時,為了使你的程序可以使用多個核心運行,這時協程就真正的是并行運行了,你必須使用?`GOMAXPROCS`?變量。
這會告訴運行時有多少個協程同時執行。
并且只有 gc 編譯器真正實現了協程,適當的把協程映射到操作系統線程。使用?`gccgo`?編譯器,會為每一個協程創建操作系統線程。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md#1413-使用-gomaxprocs)14.1.3 使用 GOMAXPROCS
在 gc 編譯器下(6g 或者 8g)你必須設置 GOMAXPROCS 為一個大于默認值 1 的數值來允許運行時支持使用多于 1 個的操作系統線程,所有的協程都會共享同一個線程除非將 GOMAXPROCS 設置為一個大于 1 的數。當 GOMAXPROCS 大于 1 時,會有一個線程池管理許多的線程。通過?`gccgo`?編譯器 GOMAXPROCS 有效的與運行中的協程數量相等。假設 n 是機器上處理器或者核心的數量。如果你設置環境變量 GOMAXPROCS>=n,或者執行?`runtime.GOMAXPROCS(n)`,接下來協程會被分割(分散)到 n 個處理器上。更多的處理器并不意味著性能的線性提升。有這樣一個經驗法則,對于 n 個核心的情況設置 GOMAXPROCS 為 n-1 以獲得最佳性能,也同樣需要遵守這條規則:協程的數量 > 1 + GOMAXPROCS > 1。
所以如果在某一時間只有一個協程在執行,不要設置 GOMAXPROCS!
還有一些通過實驗觀察到的現象:在一臺 1 顆 CPU 的筆記本電腦上,增加 GOMAXPROCS 到 9 會帶來性能提升。在一臺 32 核的機器上,設置 GOMAXPROCS=8 會達到最好的性能,在測試環境中,更高的數值無法提升性能。如果設置一個很大的 GOMAXPROCS 只會帶來輕微的性能下降;設置 GOMAXPROCS=100,使用?`top`?命令和?`H`?選項查看到只有 7 個活動的線程。
增加 GOMAXPROCS 的數值對程序進行并發計算是有好處的;
請看?[goroutine_select2.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_14/goroutine_select2.go)
總結:GOMAXPROCS 等同于(并發的)線程數量,在一臺核心數多于1個的機器上,會盡可能有等同于核心數的線程在并行運行。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md#1414-如何用命令行指定使用的核心數量)14.1.4 如何用命令行指定使用的核心數量
使用?`flags`?包,如下:
~~~
var numCores = flag.Int("n", 2, "number of CPU cores to use")
in main()
flag.Pars()
runtime.GOMAXPROCS(*numCores)
~~~
協程可以通過調用`runtime.Goexit()`來停止,盡管這樣做幾乎沒有必要。
示例 14.1-[goroutine1.go](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/examples/chapter_14/goroutine1.go)?介紹了概念:
~~~
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("In main()")
go longWait()
go shortWait()
fmt.Println("About to sleep in main()")
// sleep works with a Duration in nanoseconds (ns) !
time.Sleep(10 * 1e9)
fmt.Println("At the end of main()")
}
func longWait() {
fmt.Println("Beginning longWait()")
time.Sleep(5 * 1e9) // sleep for 5 seconds
fmt.Println("End of longWait()")
}
func shortWait() {
fmt.Println("Beginning shortWait()")
time.Sleep(2 * 1e9) // sleep for 2 seconds
fmt.Println("End of shortWait()")
}
~~~
輸出:
~~~
In main()
About to sleep in main()
Beginning longWait()
Beginning shortWait()
End of shortWait()
End of longWait()
At the end of main() // after 10s
~~~
`main()`,`longWait()`?和?`shortWait()`?三個函數作為獨立的處理單元按順序啟動,然后開始并行運行。每一個函數都在運行的開始和結束階段輸出了消息。為了模擬他們運算的時間消耗,我們使用了?`time`?包中的?`Sleep`?函數。`Sleep()`?可以按照指定的時間來暫停函數或協程的執行,這里使用了納秒(ns,符號 1e9 表示 1 乘 10 的 9 次方,e=指數)。
他們按照我們期望的順序打印出了消息,幾乎都一樣,可是我們明白這是模擬出來的,以并行的方式。我們讓?`main()`?函數暫停 10 秒從而確定它會在另外兩個協程之后結束。如果不這樣(如果我們讓?`main()`?函數停止 4 秒),`main()`?會提前結束,`longWait()`?則無法完成。如果我們不在?`main()`?中等待,協程會隨著程序的結束而消亡。
當?`main()`?函數返回的時候,程序退出:它不會等待任何其他非 main 協程的結束。這就是為什么在服務器程序中,每一個請求都會啟動一個協程來處理,`server()`?函數必須保持運行狀態。通常使用一個無限循環來達到這樣的目的。
另外,協程是獨立的處理單元,一旦陸續啟動一些協程,你無法確定他們是什么時候真正開始執行的。你的代碼邏輯必須獨立于協程調用的順序。
為了對比使用一個線程,連續調用的情況,移除 go 關鍵字,重新運行程序。
現在輸出:
~~~
In main()
Beginning longWait()
End of longWait()
Beginning shortWait()
End of shortWait()
About to sleep in main()
At the end of main() // after 17 s
~~~
協程更有用的一個例子應該是在一個非常長的數組中查找一個元素。
將數組分割為若干個不重復的切片,然后給每一個切片啟動一個協程進行查找計算。這樣許多并行的協程可以用來進行查找任務,整體的查找時間會縮短(除以協程的數量)。
## [](https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.1.md#1415-go-協程goroutines和協程coroutines)14.1.5 Go 協程(goroutines)和協程(coroutines)
(譯者注:標題中的“Go協程(goroutines)” 即是 14 章講的協程指的是 Go 語言中的協程。而“協程(coroutines)”指的是其他語言中的協程概念,僅在本節出現。)
在其他語言中,比如 C#,Lua 或者 Python 都有協程的概念。這個名字表明它和 Go協程有些相似,不過有兩點不同:
* Go 協程意味著并行(或者可以以并行的方式部署),協程一般來說不是這樣的
* Go 協程通過通道來通信;協程通過讓出和恢復操作來通信
Go 協程比協程更強大,也很容易從協程的邏輯復用到 Go 協程。
- 前言
- 第一部分:學習 Go 語言
- 第1章:Go 語言的起源,發展與普及
- 1.1 起源與發展
- 1.2 語言的主要特性與發展的環境和影響因素
- 第2章:安裝與運行環境
- 2.1 平臺與架構
- 2.2 Go 環境變量
- 2.3 在 Linux 上安裝 Go
- 2.4 在 Mac OS X 上安裝 Go
- 2.5 在 Windows 上安裝 Go
- 2.6 安裝目錄清單
- 2.7 Go 運行時(runtime)
- 2.8 Go 解釋器
- 第3章:編輯器、集成開發環境與其它工具
- 3.1 Go 開發環境的基本要求
- 3.2 編輯器和集成開發環境
- 3.3 調試器
- 3.4 構建并運行 Go 程序
- 3.5 格式化代碼
- 3.6 生成代碼文檔
- 3.7 其它工具
- 3.8 Go 性能說明
- 3.9 與其它語言進行交互
- 第二部分:語言的核心結構與技術
- 第4章:基本結構和基本數據類型
- 4.1 文件名、關鍵字與標識符
- 4.2 Go 程序的基本結構和要素
- 4.3 常量
- 4.4 變量
- 4.5 基本類型和運算符
- 4.6 字符串
- 4.7 strings 和 strconv 包
- 4.8 時間和日期
- 4.9 指針
- 第5章:控制結構
- 5.1 if-else 結構
- 5.2 測試多返回值函數的錯誤
- 5.3 switch 結構
- 5.4 for 結構
- 5.5 Break 與 continue
- 5.6 標簽與 goto
- 第6章:函數(function)
- 6.1 介紹
- 6.2 函數參數與返回值
- 6.3 傳遞變長參數
- 6.4 defer 和追蹤
- 6.5 內置函數
- 6.6 遞歸函數
- 6.7 將函數作為參數
- 6.8 閉包
- 6.9 應用閉包:將函數作為返回值
- 6.10 使用閉包調試
- 6.11 計算函數執行時間
- 6.12 通過內存緩存來提升性能
- 第7章:數組與切片
- 7.1 聲明和初始化
- 7.2 切片
- 7.3 For-range 結構
- 7.4 切片重組(reslice)
- 7.5 切片的復制與追加
- 7.6 字符串、數組和切片的應用
- 第8章:Map
- 8.1 聲明、初始化和 make
- 8.2 測試鍵值對是否存在及刪除元素
- 8.3 for-range 的配套用法
- 8.4 map 類型的切片
- 8.5 map 的排序
- 8.6 將 map 的鍵值對調
- 第9章:包(package)
- 9.1 標準庫概述
- 9.2 regexp 包
- 9.3 鎖和 sync 包
- 9.4 精密計算和 big 包
- 9.5 自定義包和可見性
- 9.6 為自定義包使用 godoc
- 9.7 使用 go install 安裝自定義包
- 9.8 自定義包的目錄結構、go install 和 go test
- 9.9 通過 Git 打包和安裝
- 9.10 Go 的外部包和項目
- 9.11 在 Go 程序中使用外部庫
- 第10章:結構(struct)與方法(method)
- 10.1 結構體定義
- 10.2 使用工廠方法創建結構體實例
- 10.3 使用自定義包中的結構體
- 10.4 帶標簽的結構體
- 10.5 匿名字段和內嵌結構體
- 10.6 方法
- 10.8 垃圾回收和 SetFinalizer
- 第11章:接口(interface)與反射(reflection)
- 11.1 接口是什么
- 11.2 接口嵌套接口
- 11.3 類型斷言:如何檢測和轉換接口變量的類型
- 11.4 類型判斷:type-switch
- 11.5 測試一個值是否實現了某個接口
- 11.6 使用方法集與接口
- 11.7 第一個例子:使用 Sorter 接口排序
- 11.8 第二個例子:讀和寫
- 11.9 空接口
- 11.10 反射包
- 第三部分:Go 高級編程
- 第12章 讀寫數據
- 12.1 讀取用戶的輸入
- 12.2 文件讀寫
- 12.3 文件拷貝
- 12.4 從命令行讀取參數
- 12.5 用buffer讀取文件
- 12.6 用切片讀寫文件
- 12.7 用 defer 關閉文件
- 12.8 使用接口的實際例子:fmt.Fprintf
- 12.9 Json 數據格式
- 12.10 XML 數據格式
- 12.11 用 Gob 傳輸數據
- 12.12 Go 中的密碼學
- 第13章 錯誤處理與測試
- 13.1 錯誤處理
- 13.2 運行時異常和 panic
- 13.3 從 panic 中恢復(Recover)
- 13.4 自定義包中的錯誤處理和 panicking
- 13.5 一種用閉包處理錯誤的模式
- 13.6 啟動外部命令和程序
- 13.7 Go 中的單元測試和基準測試
- 13.8 測試的具體例子
- 13.9 用(測試數據)表驅動測試
- 13.10 性能調試:分析并優化 Go 程序
- 第14章:協程(goroutine)與通道(channel)
- 14.1 并發、并行和協程
- 14.2 使用通道進行協程間通信
- 14.3 協程同步:關閉通道-對阻塞的通道進行測試
- 14.4 使用 select 切換協程
- 14.5 通道,超時和計時器(Ticker)
- 14.6 協程和恢復(recover)
- 第15章:網絡、模版與網頁應用
- 15.1 tcp服務器
- 15.2 一個簡單的web服務器
- 15.3 訪問并讀取頁面數據
- 15.4 寫一個簡單的網頁應用
- 第四部分:實際應用
- 第16章:常見的陷阱與錯誤
- 16.1 誤用短聲明導致變量覆蓋
- 16.2 誤用字符串
- 16.3 發生錯誤時使用defer關閉一個文件
- 16.5 不需要將一個指向切片的指針傳遞給函數
- 16.6 使用指針指向接口類型
- 16.7 使用值類型時誤用指針
- 16.8 誤用協程和通道
- 16.9 閉包和協程的使用
- 16.10 糟糕的錯誤處理
- 第17章:模式
- 17.1 關于逗號ok模式
- 第18章:出于性能考慮的實用代碼片段
- 18.1 字符串
- 18.2 數組和切片
- 18.3 映射
- 18.4 結構體
- 18.5 接口
- 18.6 函數
- 18.7 文件
- 18.8 協程(goroutine)與通道(channel)
- 18.9 網絡和網頁應用
- 18.10 其他
- 18.11 出于性能考慮的最佳實踐和建議
- 附錄