Go語言裏建立一個協程很簡單,使用go
關鍵字就可讓一個普通方法協程化:git
package main import ( "fmt" "time" ) func main(){ fmt.Println("run in main coroutine.") for i:=0; i<10; i++ { go func(i int) { fmt.Printf("run in child coroutine %d.\n", i) }(i) } //防止子協程尚未結束主協程就退出了 time.Sleep(time.Second * 1) }
下面這些概念可能不太好理解,須要慢慢理解。能夠先跳過,回頭再來看。github
概念:golang
協程
能夠理解爲純用戶態的線程,其經過協做而不是搶佔來進行切換。相對於進程或者線程,協程全部的操做均可以在用戶態完成,建立和切換的消耗更低。運行態
、就緒態
和休眠態
。同一個線程中最多隻會存在一個處於運行態的協程。就緒態協程
是指那些具有了運行能力可是尚未獲得運行機會的協程,它們隨時會被調度到運行態;休眠態的協程
還不具有運行能力,它們是在等待某些條件的發生,好比 IO 操做的完成、睡眠時間的結束等。協程通常用 TCP/HTTP/RPC服務、消息推送系統、聊天系統等。使用協程,咱們能夠很方便的搭建一個支持高併發的TCP或HTTP服務端。緩存
通道的英文是Channels,簡稱chan
。何時要用到通道呢?能夠先簡單的理解爲:協程
在須要協做通訊的時候就須要用通道。安全
在GO裏,不一樣的並行協程之間交流的方式有兩種,一種是經過共享變量,另外一種是經過通道。Go 語言鼓勵使用通道的形式來交流。bash
舉個簡單的例子,咱們使用協程實現併發調用遠程接口,最終咱們須要把每一個協程請求回來的數據進行彙總一塊兒返回,這個時候就用到通道了。數據結構
建立通道
(channel)只能使用make
函數:併發
c := make(chan int)
通道
是區分類型的,如這裏的int
。異步
Go 語言爲通道的讀寫設計了特殊的箭頭語法糖 <-
,讓咱們使用通道時很是方便。把箭頭寫在通道變量的右邊就是寫通道,把箭頭寫在通道的左邊就是讀通道。一次只能讀寫一個元素。函數
c := make(chan bool) c <- true //寫入 <- c //讀取
上面咱們介紹了默認的非緩存類型的channel,不過Go也容許指定channel的緩衝大小,很簡單,就是channel能夠存儲多少元素:
c := make(chan int, value)
當 value = 0
時,通道
是無緩衝阻塞讀寫的,等價於make(chan int)
;當value > 0
時,通道
有緩衝、是非阻塞的,直到寫滿 value
個元素才阻塞寫入。具體說明下:
非緩衝通道
不管是發送操做仍是接收操做,一開始執行就會被阻塞,直到配對的操做也開始執行纔會繼續傳遞。因而可知,非緩衝通道是在用同步的方式傳遞數據。也就是說,只有收發雙方對接上了,數據纔會被傳遞。數據是直接從發送方複製到接收方的,中間並不會用非緩衝通道作中轉。
緩衝通道
緩衝通道能夠理解爲消息隊列,在有容量的時候,發送和接收是不會互相依賴的。用異步的方式傳遞數據。
下面咱們用一個例子來理解一下:
package main import "fmt" func main() { var c = make(chan int, 0) var a string go func() { a = "hello world" <-c }() c <- 0 fmt.Println(a) }
這個例子輸出的必定是hello world
。可是若是你把通道的容量由0改成大於0的數字,輸出結果就不必定是hello world
了,極可能是空。爲何?
當通道是無緩衝通道時,執行到c <- 0
,通道滿了,寫操做會被阻塞住,直到執行<-c
解除阻塞,後面的語句接着執行。
要是改爲非阻塞通道,執行到c <- 0
,發現還能寫入,主協程就不會阻塞了,但這時候輸出的是空字符串仍是hello world
,取決因而子協程和主協程哪一個運行的速度快。
通道做爲容器,它能夠像切片同樣,使用
cap()
和len()
全局函數得到通道的容量和當前內部的元素個數。
上一節"協程"的例子裏,咱們在主協程里加了個time.Sleep()
,目的是防止子協程尚未結束主協程就退出了。可是對於實際生活的大多數場景來講,1秒是不夠的,而且大部分時候咱們都沒法預知for循環內代碼運行時間的長短。這時候就不能使用time.Sleep()
來完成等待操做了。下面咱們用通道來改寫:
package main import ( "fmt" ) func main() { fmt.Println("run in main coroutine.") count := 10 c := make(chan bool, count) for i := 0; i < count; i++ { go func(i int) { fmt.Printf("run in child coroutine %d.\n", i) c <- true }(i) } for i := 0; i < count; i++ { <-c } }
默認的通道是支持讀寫的,咱們能夠定義單向通道:
//只讀 var readOnlyChannel = make(<-chan int) //只寫 var writeOnlyChannel = make(chan<- int)
下面是一個示例,咱們模擬消息隊列的消費者、生產者:
package main import ( "fmt" "time" ) func Producer(c chan<- int) { for i := 0; i < 10; i++ { c <- i } } func Consumer1(c <-chan int) { for m := range c { fmt.Printf("oh, I get luckly num: %v\n", m) } } func Consumer2(c <-chan int) { for m := range c { fmt.Printf("oh, I get luckly num too: %v\n", m) } } func main() { c := make(chan int, 2) go Consumer1(c) go Consumer2(c) Producer(c) time.Sleep(time.Second) }
對於生產者,咱們但願通道是隻寫屬性,而對於消費者則是隻讀屬性,這樣避免對通道進行錯誤的操做。固然,若是你將本例裏消費者、生產者的通道單向屬性去掉也是能夠的,沒什麼問題:
func Producer(c chan int) {} func Consumer1(c chan int) {} func Consumer2(c chan int) {}
事實上
channel
只讀或只寫都沒有意義,所謂的單向channel
其實只是方法裏聲明時用,若是後續代碼裏,向原本用於讀channel
裏寫入了數據,編譯器會提示錯誤。
讀取一個已經關閉的通道會當即返回通道類型的零值
,而寫一個已經關閉的通道會拋異常。若是通道里的元素是整型的,讀操做是不能經過返回值來肯定通道是否關閉的。
一、如何安全的讀通道,確保不是讀取的已關閉通道的零值
?
答案是使用for...range
語法。當通道爲空時,循環會阻塞;當通道關閉,循環會中止。經過循環中止,咱們能夠認爲通道已經關閉。示例:
package main import "fmt" func main() { var c = make(chan int, 3) //子協程寫 go func() { c <- 1 close(c) }() //直接讀取通道,存在不知道子協程是否已關閉的狀況 //fmt.Println(<-c) //fmt.Println(<-c) //主協程讀取:使用for...range安全的讀取 for value := range c { fmt.Println(value) } }
輸出:
1
二、如何安全的寫通道,確保不會寫入已關閉的通道?
Go 語言並不存在一個內置函數能夠判斷出通道是否已經被關閉。確保通道寫安全的最好方式是由負責寫通道的協程本身來關閉通道,讀通道的協程不要去關閉通道。
可是這個方法只能解決單寫多讀的場景。若是遇到多寫單讀的狀況就有問題了:沒法知道其它寫協程何時寫完,那麼也就不能肯定何時關閉通道。這個時候就得額外使用一個通道專門作這個事情。
咱們可使用內置的 sync.WaitGroup
,它使用計數來等待指定事件完成:
package main import ( "fmt" "sync" "time" ) func main() { var ch = make(chan int, 8) //寫協程 var wg = new(sync.WaitGroup) for i := 1; i <= 4; i++ { wg.Add(1) go func(num int, ch chan int, wg *sync.WaitGroup) { defer wg.Done() ch <- num ch <- num * 10 }(i, ch, wg) } //讀 go func(ch chan int) { for num := range ch { fmt.Println(num) } }(ch) //Wait阻塞等待全部的寫通道協程結束,待計數值變成零,Wait纔會返回 wg.Wait() //安全的關閉通道 close(ch) //防止讀取通道的協程尚未完畢 time.Sleep(time.Second) fmt.Println("finish") }
輸出:
3 30 2 20 1 10 4 40 finish
有時候還會遇到多個生產者,只要有一個生產者就緒,消費者就能夠進行消費的狀況。這個時候可使用go語言提供的select
語句,它能夠同時管理多個通道讀寫,若是全部通道都不能讀寫,它就總體阻塞,只要有一個通道能夠讀寫,它就會繼續。示例:
package main import ( "fmt" "time" ) func main() { var ch1 = make(chan int) var ch2 = make(chan int) fmt.Println(time.Now().Format("15:04:05")) go func(ch chan int) { time.Sleep(time.Second) ch <- 1 }(ch1) go func(ch chan int) { time.Sleep(time.Second * 2) ch <- 2 }(ch2) for { select { case v := <-ch1: fmt.Println(time.Now().Format("15:04:05") + ":來自ch1:", v) case v := <-ch2: fmt.Println(time.Now().Format("15:04:05") + ":來自ch2:", v) //default: //fmt.Println("channel is empty !") } } }
輸出:
13:39:56 13:39:57:來自ch1: 1 13:39:58:來自ch2: 2 fatal error: all goroutines are asleep - deadlock!
默認select
處於阻塞狀態,1s後,子協程1完成寫入,主協程讀出了數據;接着子協程2完成寫入,主協程讀出了數據;接着主協程掛掉了,緣由是主協程發如今等一個永遠不會來的數據,這顯然是沒有結果的,乾脆就直接退出了。
若是把註釋的部分打開,那麼程序在打印出來自ch一、ch2的數據後,就會一直執行default
裏面的程序。這個時候程序不會退出。緣由是當 select
語句全部通道都不可讀寫時,若是定義了 default
分支,那就會執行 default
分支邏輯。
注:
select{}
代碼塊是一個沒有任何case
的select
,它會一直阻塞。
golang中chan的應用場景總結
https://github.com/nange/blog/issues/9
Go語言之Channels實際應用
https://www.s0nnet.com/archives/go-channels-practice
通道原理部分能夠根據文末給出的參考連接
《快學 Go 語言》第 12 課 —— 通道
去查看。
go語言裏的map
是線程不安全的:
package main import "fmt" func write(d map[string]string) { d["name"] = "yujc" } func read(d map[string]string) { fmt.Println(d["name"]) } func main() { d := map[string]string{} go read(d) write(d) }
Go 語言內置了數據結構競態檢查
工具來幫咱們檢查程序中是否存在線程不安全的代碼,只要在運行的時候加上-race
參數便可:
$ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c0000a8180 by goroutine 6: ... yujc Found 2 data race(s) exit status 66
能夠看出,上面的代碼存在安全隱患。
咱們可使用sync.Mutex
來保護map
,原理是在每次讀寫操做以前使用互斥鎖進行保護,防止其餘線程同時操做:
package main import ( "fmt" "sync" ) type SafeDict struct { data map[string]string mux *sync.Mutex } func NewSafeDict(data map[string]string) *SafeDict { return &SafeDict{ data: data, mux: &sync.Mutex{}, } } func (d *SafeDict) Get(key string) string { d.mux.Lock() defer d.mux.Unlock() return d.data[key] } func (d *SafeDict) Set(key string, value string) { d.mux.Lock() defer d.mux.Unlock() d.data[key] = value } func main(){ dict := NewSafeDict(map[string]string{}) go func(dict *SafeDict) { fmt.Println(dict.Get("name")) }(dict) dict.Set("name", "yujc") }
運行檢測:
$ go run -race main.go yujc
上面的代碼若是不使用-race
運行,不必定會有結果,取決於主協程、子協程哪一個先運行。
注意:
sync.Mutex
是一個結構體對象,這個對象在使用的過程當中要避免被淺拷貝,不然起不到保護做用。應儘可能使用它的指針類型。
上面的代碼裏咱們多處使用了d.mux.Lock()
,可否簡化成d.Lock()
呢?答案是能夠的。咱們知道,結構體能夠自動繼承匿名內部結構體的全部方法:
type SafeDict struct { data map[string]string *sync.Mutex } func NewSafeDict(data map[string]string) *SafeDict { return &SafeDict{data, &sync.Mutex{}} } func (d *SafeDict) Get(key string) string { d.Lock() defer d.Unlock() return d.data[key] }
這樣就完成了簡化。
對於讀多寫少的場景,可使用讀寫鎖
代替互斥鎖
,能夠提升性能。
讀寫鎖提供了下面4個方法:
Lock()
寫加鎖Unlock()
寫釋放鎖RLock()
讀加鎖RUnlock()
讀釋放鎖寫鎖
是排它鎖
,加寫鎖
時會阻塞其它協程再加讀鎖
和寫鎖
;讀鎖
是共享鎖
,加讀鎖還能夠容許其它協程再加讀鎖
,可是會阻塞加寫鎖
。讀寫鎖
在寫併發高的狀況下性能退化爲普通的互斥鎖
。
咱們把上節中的互斥鎖換成讀寫鎖:
package main import ( "fmt" "sync" ) type SafeDict struct { data map[string]string *sync.RWMutex } func NewSafeDict(data map[string]string) *SafeDict { return &SafeDict{data, &sync.RWMutex{}} } func (d *SafeDict) Get(key string) string { d.RLock() defer d.RUnlock() return d.data[key] } func (d *SafeDict) Set(key string, value string) { d.Lock() defer d.Unlock() d.data[key] = value } func main(){ dict := NewSafeDict(map[string]string{}) go func(dict *SafeDict) { fmt.Println(dict.Get("name")) }(dict) dict.Set("name", "yujc") }
改完後,使用競態檢測工具檢測仍是能經過的。
一、make(chan int) 和 make(chan int, 1) 的區別
https://www.jianshu.com/p/f12e1766c19f
二、channel
https://www.jianshu.com/p/4d97dc032730
三、《快學 Go 語言》第 12 課 —— 通道
https://mp.weixin.qq.com/s?__biz=MzI0MzQyMTYzOQ==&mid=2247484601&idx=1&sn=97c0de2acc3127c9e913b6338fa65737
四、《快學 Go 語言》第 13 課 —— 併發與安全
https://mp.weixin.qq.com/s?__biz=MzI0MzQyMTYzOQ==&mid=2247484683&idx=1&sn=966cb818f034ffd4538eae7a61cd0c58