Go語言之goroutine和通道
goroutine
在Go里,每一個並發執行的活動稱為goroutine
。 如果你是一名Java程式設計師,可以把goroutine
比作為執行緒,但是goroutine
和執行緒在數量上有很大的差別,原因在於Go語言引入了協程的概念,協程相比於執行緒是一種用戶態的執行緒,協程更加輕量,實用更加經濟,因此同樣的伺服器可以開銷的協程數量要比執行緒多很多。
goroutine和協程的區別:
- goroutine是協程的go語言實現,相當於把別的語言的類庫的功能內置到語言里。從調度上看,goroutine的調度開銷遠遠小於執行緒調度開銷。
- 不同的是:Golang在runtime,系統調用等多方面對goroutine調度進行了封裝和處理,即goroutine不完全是用戶控制,一定程度上由go運行時(runtime)管理,好處:當某goroutine阻塞時,會讓出CPU給其他goroutine。
執行緒和goroutine的區別:
- OS的執行緒由OS內核調度,每隔幾毫秒,一個硬體時鐘中斷髮到CPU,CPU調用一個調度器內核函數。這個函數暫停當前正在運行的執行緒,把他的暫存器資訊保存到記憶體中,查看執行緒列表並決定接下來運行哪一個執行緒,再從記憶體中恢復執行緒的註冊表資訊,最後繼續執行選中的執行緒。這種執行緒切換需要一個完整的上下文切換:即保存一個執行緒的狀態到記憶體,再恢復另外一個執行緒的狀態,最後更新調度器的數據結構。某種意義上,這種操作還是很慢的。
- 從調度上講,執行緒的調度由 OS 的內核完成;執行緒的切換需要CPU暫存器和記憶體的數據交換,在執行緒切換的過程中需要保存/恢復所有的暫存器資訊,比如16個通用暫存器,PC(Program Counter),SP(Stack Pointer),段暫存器等等,從而切換不同的執行緒上下文。 其觸發方式為 CPU時鐘。而goroutine 的調度 則比較輕量級,由go自身的調度器完成;Go運行的時候包涵一個自己的調度器,這個調度器使用一個稱為一個M:N調度技術,m個goroutine到n個os執行緒(可以用GOMAXPROCS來控制n的數量),Go的調度器不是由硬體時鐘來定期觸發的,而是由特定的go語言結構來觸發的,他不需要切換到內核語境,所以調度一個goroutine比調度一個執行緒的成本低很多。其只關心當前go程式內協程的調度;觸發方式為 go內部的事件,如文件和網路操作垃圾回收,time.sleep,通道阻塞,互斥量操作等。在同一個原生執行緒里,若當前goroutine不發生阻塞,那麼不會主動讓出CPU給其他同一執行緒的goroutine的。在go程式啟動時,會首先創建一個特殊的內核執行緒sysmom,負責監控和調度。
- 從棧空間上,goroutine的棧空間更加動態靈活。每個OS的執行緒都有一個固定大小的棧記憶體,通常是2MB,棧記憶體用於保存在其他函數調用期間哪些正在執行或者臨時暫停的函數的局部變數。這個固定的棧大小,如果對於goroutine來說,可能是一種巨大的浪費。作為對比goroutine在生命周期開始只有一個很小的棧,典型情況是2KB, 在go程式中,一次創建十萬左右的goroutine也不罕見(2KB*100,000=200MB)。而且goroutine的棧不是固定大小,它可以按需增大和縮小,最大限制可以到1GB。
- goroutine沒有一個特定的標識。在大部分支援多執行緒的作業系統和程式語言中,執行緒有一個獨特的標識,通常是一個整數或者指針,這個特性可以讓我們構建一個執行緒的局部存儲,本質是一個全局的map,以執行緒的標識作為鍵,這樣每個執行緒可以獨立使用這個map存儲和獲取值,不受其他執行緒干擾。goroutine中沒有可供程式設計師訪問的標識,原因是一種純函數的理念,不希望濫用執行緒局部存儲導致一個不健康的超距作用,即函數的行為不僅取決於它的參數,還取決於運行它的執行緒標識。
簡單的示例程式碼如下:在Go里,每一個並發執行的活動稱為goroutine
。
簡單的示例程式碼如下:
f() //調用f();等它返回
go f() // 新建一個調用分()的goroutine,不用等待
在下面的例子中,主goroutine
計算第45個斐波那契數。因為它使用非常低效的遞歸演算法,因此需要大量的時間來執行,在此期間我們提供一個可見的提示,顯示一個字元串」spinner「來指示程式依然在運行。
package main
import (
"fmt"
"time"
)
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
func main() {
go spinner(100 * time.Microsecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
若干秒後,fib(45)返回結果,如下圖所示:
然後main
函數返回,所有的goroutine
都暴力地直接終結,然後程式退出。
通道
如果說goroutine
是Go程式並發的執行體,通道就是它們之間的連接。通道是可以讓一個goroutine
發送特定值到另一個goroutine
的通訊機制。每一個通道是一個具體類型的導管,叫做通道的元素類型。一個有int
類型元素的通道寫為chan int
。
使用內置的make
函數來創建一個通道:
ch := make(chan int) // ch的類型是 chan int
像map
一樣,通道是一個使用make
創建的數據結構的引用。當複製或者作為參數傳遞到一個函數時,複製的是引用,這樣調用者和被調用者都引用同一份數據結構。和其他引用類型一樣,通道的零值是nil
。
通道有兩個主要的操作:發送和接收,這兩者統稱為通訊。send
語句從一個goroutine
傳輸一個值到另一個在執行接收表達式的goroutine
。兩個操作都使用<-
操作符書寫。發送語句中,通道和值分別在<-
的左右兩邊。在接收表達式中,<-
放在通道操作數的前面。
具體書寫格式如下:
ch <- x //發送語句
x = <- ch // 賦值語句中的接收表達式
<- ch // 接收語句,丟棄結果
通道支援第三個操作:關閉,它設置一個標誌位來指示值當前已經發送完畢,這個通道後面沒有值了,關閉後的發送操作將導致宕機。在一個已經關閉的通道上進行接收操作,將獲取所有已經發送的值,直到通道為空,這是任何接收操作會立即完成,同時獲取到一個通道元素類型對應的零值。
調用內置的close
函數來關閉通道:
close(ch)
無緩衝通道
使用簡單的make
調用創建的通道叫做無緩衝通道,但make
還可以接受第二個可選參數,一個表示通道容量的整數。如果容量是0,make
創建一個無緩衝的通道。
ch = make(chan int) // 無緩衝通道
ch = make(chan int, 0) // 無緩衝通道
ch = make(chan int, 3) // 容量為3的緩衝通道
無緩衝通道上的發送操作將會阻塞,直到另一個goroutine
在對應的通道上執行接收操作,這時值傳送完成,兩個goroutine
都可以繼續執行。相反,如果接收操作先執行,接收方goroutine
將阻塞,直到另一個goroutine
在同一個通道發送一個值。
使用無緩衝通道進行通訊導致發送和接收goroutine
同步化。因此,無緩衝通道也稱為同步通道。當一個值在無緩衝通道上傳遞時,接收值後發送方goroutine
才被再次喚醒。
package main
import "fmt"
func main(){
ch:=make(chan int) //這裡就是創建了一個channel,這是無緩衝管道注意
go func(){ //創建子go程
for i:=0;i<=6;i++{
ch<-i //循環寫入管道
fmt.Println("寫入",i)
}
}()
for i:=0;i<6;i++{ //主go程
num:=<-ch //循環讀出管道
fmt.Println("讀出",num)
}
}
緩衝通道
緩衝通道有一個元素隊列,隊列的最大長度在創建的時候通過make
的容量參數來設置。如下程式碼創建了一個帶有10個字元串的緩衝通道:
ch = make(chan string,10)
緩衝通道上的發送操作在對列的尾部插入一個元素,接收操作從隊列的頭部移除一個元素。如果通道滿了,發送操作會阻塞所在的goroutine
直到另一個goroutine
對它進行接收操作來騰出可用的空間。反過來,如果通道是空的,執行接收操作的goroutine
阻塞,直到另一個goroutine
在通道上發送數據。
現在,我們可以在通道上無阻塞的發送三個值,但是在發送第四個值的時候就會阻塞。
package main
func main() {
ch := make(chan string, 3)
ch <- "A"
ch <- "B"
ch <- "C"
ch <- "D"
}
在我們向管道塞入第四個值的時候,程式爆出了死鎖的異常,如下圖:
但是當我們在執行第四次向通道塞值的時候,從通道取出一個值,就可以安全的進行第四次塞值了,並且成功的列印出了隊列的第一個元素A,如下圖:
管道
通道可以用來連接goroutine
,這樣一個具體的輸出是另一個的輸入。管道一般是由三個goroutine
組成,使用兩個通道連接起來。
如下程式碼所示:
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
// counter
go func() {
for x := 0; x< 100; x++ {
naturals <- x
}
close(naturals)
}()
// squares
go func() {
for {
x,ok := <-naturals
if !ok {
break // 通道關閉並且讀完
}
squares <- x * x
}
close(squares)
}()
// printer(在主goroutine中)
for x := range squares{
fmt.Println(x)
}
}
結束時,關閉每一個通道不是必需的,只有在通知接收方goroutine
所有數據都發送完畢的時候才需要關閉通道。通道也是可以通過垃圾回收器根據它是否可以訪問來決定是否回收它,而不是根據它是否關閉。
不要將這個close
操作和對於文件的close
操作混淆。當結束的時候對每一個文件調用Close
方法是非常重要的。
單向通道
Go也提供了單向通道類型,僅僅導出發送或者接收操作。類型chan <- int
是一個只能發送的通道,允許接收但是不能發送。(<-
操作符相對於chan
關鍵字的位置是一個幫助記憶的點)。
package main
import "fmt"
// 單向輸出通道 chan<-
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
}
// 單向輸出通道 chan<-
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
}
// 單向輸入通道 <-chan
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
並行循環
為了演示並行循環,我們考慮生成一批全尺寸影像的縮略圖,程式碼如下:
package main
import (
"gopl.io/ch8/thumbnail"
"io/ioutil"
"log"
)
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
func main() {
rootPath := "W:\\Google_Download\\壁紙\\動漫壁紙\\"
//var rootPath string = "W:\\Google_Download\\壁紙\\動漫壁紙\\"
files, _ := ioutil.ReadDir(rootPath)
var fileNames []string
/*
第一種讀取文件列表的方法
*/
for _, f := range files {
//fmt.Println(f.Name())
fileNames = append(fileNames, rootPath+f.Name())
}
makeThumbnails(fileNames)
}
其中需要導入的gopl.io/ch8/thumbnail
包,從下面的網站下載:
GitHub – adonovan/gopl.io: Example programs from “The Go Programming Language”](//github.com/adonovan/gopl.io/)
生成後的結果如圖所示,都是我喜歡的動漫圖片,如果也喜歡,私信我給你發哈~
以上就是Go語言關於goroutine
和通道的內容,關於goroutine
和通道其實還有很多可以深挖的東西,我們後面會繼續學習。希望這篇文章可以幫助到你~