文章目錄
- 寫在前面
- 關于并發
- Go協程與通道
- 在Go中使用協程
- 使用channel在協程間通信
- 使用select來切換協程操作
寫在前面
提及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)
是通道可以同時容納的元素的個數,如果容量大于 0,通道就是異步的了:緩沖滿載(發送)或變空(接收)之前通信不會阻塞,元素會按照發送的順序被接收buf
- 使用
來操作channelfor-range
可以用在通道上,以便從通道中擷取值: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)
}
}
}