寫在前面
提及Go,大家都會人云亦云一句“Go支持高併發,適合使用高併發的場景”,事實也確實如此,Go學習筆記系列也終於到了該介紹下最著名的Go併發的時候了,沒有介紹併發的Go文章是沒有靈魂的哈哈^^
關於併發
提及併發,很容易聯想到另外一個概念:並行。它們兩個的區別是:
- 併發主要由切換時間片來實現多個任務“同時”運行
- 並行是直接通過多核實現多個任務同時運行
一個併發程序可以只在一個處理器或內核上運行多個任務,但是某一時間點只有一個任務在運行;如果運行在多處理器或多核上,就可以在同一時間點有多個任務在運行,才能實現真正的並行,因此,併發程序可以是運行在單處理器(單核)上也可以是運行在多處理器(多核)上。而在Go中,可以設置核數,讓併發程序在多核心上真正並行運行,充分發揮多核計算機的能力
Go協程與通道
在其它編程語言中,實現併發程序往往是使用多線程的技術。在一個進程中有多個線程,它們共享同一個內存地址空間。然而使用多線程難以做到準確,尤其是內存中數據共享的問題,它們會被多線程以無法預知的方式進行操作,導致一些無法重現或者隨機的結果。多線程解決這個問題的方式是同步不同的線程,對數據加鎖
但是,這會帶來更高的複雜度,更容易使代碼出錯以及更低的性能,所以這個經典的方法明顯不再適合現代多核/多處理器編程
在Go中,使用協程(goroutines)來實現併發程序:
- 協程是輕量級的,比線程要更輕,只需要使用少量的內存和資源(每個實例4-5k左右的內存棧),因此在必要時可以創建大量的協程,實現高併發
- 協程與操作系統線程之間沒有一對一的關係,協程是根據一個或多個線程的可用性,映射(多路複用)在它們之上的
- Go多個協程之間使用通道(channel)來同步(它不通過共享內存來通信,而是通過通信來共享內存),當然Go中也提供了
sync
包可以進行傳統的加鎖同步操作,但並不推薦使用
在Go中使用協程
在Go中使用協程是通過關鍵字go調用一個函數或者方法來實現的:
import (
"fmt"
"time"
)
func main(){
go Goroutine()
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
根據上面的代碼,我們定義了一個Goroutine
的函數,並在主程序中使用go
關鍵字去調用該函數,從而啓動一個協程去執行Goroutine
這個函數。在程序中使用time.Sleep(3*time.Second)
是爲了讓主程序延時3s再結束,否則主程序啓動完一個協程後立即退出,我們將沒法看到協程函數中打印的信息
新版本的Go(應該是1.8之後)當我們啓動多個協程時,Go將會自動啓動多個核心來並行運行,而在老版本的Go裏需要我們手動設置,手動設置多核心的操作如下:
import (
"fmt"
"runtime"
"time"
)
func main(){
num := runtime.NumCPU()
runtime.GOMAXPROCS(num) //新版本會自動設置
for i := 0; i < 10; i++ {
go Goroutine(i)
}
time.Sleep(3 * time.Second)
}
func Goroutine(){
fmt.Println("start a goroutine")
}
使用channel在協程間通信
通道(channel)是一種特殊的類型,可以理解爲發送某種其它類型數據的管道,用於在協程之間通信。數據利用通道進行傳遞,在任何時間,通道中的數據只能被一個協程進行訪問,因此不會發生數據競爭
- channel通過
make
進行創建,使用close
來關閉
如果只是聲明channel,未初始化,它的值爲var ch1 chan string ch1 = make(chan string) //創建一個用於傳遞string類型的通道
nil
上面兩行代碼也可以簡寫爲ch1 := make(chan string)
- 通道的操作符
<-
- 往通道發送數據:
ch <- int1
表示把變量int1
發送到通道ch
中 - 從通道接收數據:
int2 <- ch
表示變量int2
從通道ch
中接收數據(如果int2
沒有事先聲明過,則要用int2 := <- ch
)。直接使用<-ch
也可以,也表示從通道中取值,然後該值會被丟棄
上面的代碼創建了一個布爾型的channel,主程序啓動協程後就一直阻塞在func main(){ c := make(chan bool) go func() { //使用匿名函數,閉包,所以可以獲取到外層的channel變量 fmt.Println("go go go") c <- true }() <-c //阻塞,直到從通道取出數據 }
<-c
那裏等待從通道中取出數據,協程中當打印完數據後,就往通道中發送true
。主程序此時方從通道中取出數據,退出程序。從而不需要手動讓主程序睡眠等待協程完成 - 往通道發送數據:
- 大多數情況下channel默認都是阻塞的:從channel取數據一端會阻塞等待channel有數據可以取出才往下執行(如上一段代碼中所示);往channel發送數據一端需要一直阻塞等到數據從channel取出才往下執行。如果把上面那段代碼中往channel中讀取數據的位置調換一下,程序依舊會正常輸出
func main(){ c := make(chan bool) go func() { //使用匿名函數,閉包,所以可以獲取到外層的channel變量 fmt.Println("go go go") <-c }() c <- true //這裏把數據傳入通道後也會阻塞知道通道中數據被取出 }
- 根據需要,channel也可以被設置爲有緩存的,有緩存的channel在通道被填滿之前不會阻塞(異步)。上面的程序,如果設置爲有緩存的channel,那麼主程序往通道中發送數據之後就直接退出了
func main(){ c := make(chan bool, 1) go func() { //使用匿名函數,閉包,所以可以獲取到外層的channel變量 fmt.Println("go go go") <-c }() c <- true //這裏往通道發完數據就直接退出了 }
make(chan type, buf)
這裏buf
是通道可以同時容納的元素的個數,如果容量大於 0,通道就是異步的了:緩衝滿載(發送)或變空(接收)之前通信不會阻塞,元素會按照發送的順序被接收 - 使用
for-range
來操作channel
for-range
可以用在通道上,以便從通道中獲取值:
它從指定的通道中讀取數據直到通道關閉才能執行下面的代碼,因此程序必須在某個地方for v := range ch { fmt.Println(v) }
close
該通道,否則程序將死鎖func main(){ c:=make(chan bool) go func(){ fmt.Println("gogogo") c <- true close(c) }() for v := range c{ fmt.Println(v) } }
此外,關於channel還需要注意:
- channel可以設置爲單向(只讀或只寫)或雙向通道(能讀能寫),默認是雙向的
- channel是引用類型
- 一個channel只能傳遞一種類型的數據
使用select來切換協程操作
使用select
可以從不同的併發執行的協程中獲取值,它和switch
語句很類似。select
可以用來監聽進入通道的數據,也可以向通道發送數據
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
select
的功能其實就是處理列出的多個通信中的一個
default
語句是可選的,fallthrough
是不允許的,任何一個case
中執行了break
或者return
語句,select
就結束了- 如果所有
case
的通道都阻塞了,會等待直到其中一個可以處理 - 如果有多個
case
的通道可以處理,會隨機
選擇一個處理 - 如果沒有通道操作可以處理並且寫了
default
語句,它就會執行default
語句 - 在
select
中使用發送操作並且有default
可以確保發送不被阻塞!如果沒有default
,select
就會一直阻塞 select
也可以設置超時處理
下面的代碼是一個類似生產者-消費者的模式,包括了兩個通道和三個協程,其中協程goroutine1
和goroutine2
分別往通道ch1
和ch2
中寫入數據,協程goroutine3
則通過select
分別從兩個通道中讀出數據並輸出
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go goroutine1(ch1)
go goroutine2(ch2)
go goroutine3(ch1, ch2)
time.Sleep(1e9)
}
func goroutine1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func goroutine2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func goroutine3(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}