提到Go語言的併發,就不得不提goroutine,其做爲Go語言的一大特點,在平常開發中使用不少。git
在平常應用場景就會涉及一個goroutine啓動或結束,啓動一個goroutine很簡單隻須要在函數前面加關鍵詞go便可,而因爲每一個goroutine都是獨立運行的,其退出有自身決定的,除非main主程序結束或程序崩潰的狀況發生。github
那麼,如何控制goroutine或者說通知goroutine結束運行呢?golang
解決的方式其實很簡單,那就是想辦法和goroutine通信,通知goroutine何時結束,goroutine結束也能夠通知其餘goroutine或main主程序。併發
全局變量函數
channel.net
WaitGroupcode
context對象
這是併發控制最簡單的實現方式blog
一、聲明一個全局變量。接口
二、全部子goroutine共享這個變量,並不斷輪詢這個變量檢查是否有更新;
三、在主進程中變動該全局變量;
四、子goroutine檢測到全局變量更新,執行相應的邏輯。
示例
package main import ( "fmt" "time" ) func main() { open := true go func() { for open { println("goroutineA running") time.Sleep(1 * time.Second) } println("goroutineA exit") }() go func() { for open { println("goroutineB running") time.Sleep(1 * time.Second) } println("goroutineB exit") }() time.Sleep(2 * time.Second) open = false time.Sleep(2 * time.Second) fmt.Println("main fun exit") }
輸出
goroutineA running
goroutineB running
goroutineA running
goroutineB running
goroutineB running
goroutineA exit
goroutineB exit
main fun exit
這種實現方式
優勢:實現簡單。
缺點:適用一些邏輯簡單的場景,全局變量的信息量比較少,爲了防止不一樣goroutine同時修改變量須要用到加鎖來解決。
channel是goroutine之間主要的通信方式,通常會和select搭配使用。
如想了解channel實現原理可參考
https://github.com/guyan0319/...
一、聲明一個stop
的chan。
二、在goroutine中,使用select判斷stop
是否能夠接收到值,若是能夠接收到,就表示能夠退出中止了;若是沒有接收到,就會執行default
裏邏輯。直到收到stop
的通知。
三、主程序發送了stop<- true
結束的指令後。
四、子goroutine接到結束指令case <-stop退出return。
示例
package main import ( "fmt" "time" ) func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("goroutine exit") return default: fmt.Println("goroutine running") time.Sleep(1 * time.Second) } } }() time.Sleep(2 * time.Second) stop <- true time.Sleep(2 * time.Second) fmt.Println("main fun exit") }
輸出
goroutine running
goroutine running
goroutine running
goroutine exit
main fun exit
這種select+chan是一種比較優雅的併發控制方式,但也有侷限性,如多個goroutine 須要結束,以及嵌套goroutine 的場景。
Go語言提供同步包(sync),源碼(src/sync/waitgroup.go)。
Sync包同步提供基本的同步原語,如互斥鎖。除了Once和WaitGroup類型以外,大多數類型都是供低級庫例程使用的。經過Channel和溝通能夠更好地完成更高級別的同步。而且此包中的值在使用事後不要拷貝。
Sync.WaitGroup是一種實現併發控制方式,WaitGroup
對象內部有一個計數器,最初從0開始,它有三個方法:Add(), Done(), Wait()
用來控制計數器的數量。
Add(n)
把計數器設置爲n
。Done()
每次把計數器-1
。wait()
會阻塞代碼的運行,直到計數器地值減爲0。示例
package main import ( "fmt" "sync" "time" ) func main() { //定義一個WaitGroup var wg sync.WaitGroup //計數器設置爲2 wg.Add(2) go func() { time.Sleep(2 * time.Second) fmt.Println("goroutineA finish") //計數器減1 wg.Done() }() go func() { time.Sleep(2 * time.Second) fmt.Println("goroutineB finish") //計數器減1 wg.Done() }() //會阻塞代碼的運行,直到計數器地值減爲0。 wg.Wait() time.Sleep(2 * time.Second) fmt.Println("main fun exit") }
這種控制併發的方式適用於,好多個goroutine協同作一件事情的時候,由於每一個goroutine作的都是這件事情的一部分,只有所有的goroutine都完成,這件事情纔算是完成,這是等待的方式。WaitGroup相對於channel併發控制方式比較輕巧。
注意:
一、計數器不能爲負值
二、WaitGroup對象不是一個引用類型
應用場景:在 Go http 包的 Server 中,每一個Request都須要開啓一個goroutine作一些事情,這些goroutine又可能會開啓其餘的goroutine。因此咱們須要一種能夠跟蹤goroutine的方案,才能夠達到控制他們的目的,這就是Go語言爲咱們提供的Context,稱之爲上下文。
控制併發的實現方式:
一、 context.Background():返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。
二、context.WithCancel(context.Background()),建立一個可取消的子Context,而後看成參數傳給goroutine使用,這樣就可使用這個子Context跟蹤這個goroutine。
三、在goroutine中,使用select調用<-ctx.Done()
判斷是否要結束,若是接收到值的話,就能夠返回結束goroutine了;若是接收不到,就會繼續進行監控。
四、cancel(),取消函數(context.WithCancel()返回的第二個參數,名字和聲明的名字一致)。做用是給goroutine發送結束指令。
示例:
package main import ( "fmt" "time" "golang.org/x/net/context" ) func main() { //建立一個可取消子context,context.Background():返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。 ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { //使用select調用<-ctx.Done()判斷是否要結束 case <-ctx.Done(): fmt.Println("goroutine exit") return default: fmt.Println("goroutine running.") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("main fun exit") //取消context cancel() time.Sleep(5 * time.Second) }
輸出:
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
main fun exit
goroutine exit
若是想控制多個goroutine ,也很簡單。
示例
package main import ( "fmt" "time" "golang.org/x/net/context" ) func main() { //建立一個可取消子context,context.Background():返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。 ctx, cancel := context.WithCancel(context.Background()) ctxTwo, cancelTwo := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { //使用select調用<-ctx.Done()判斷是否要結束 case <-ctx.Done(): fmt.Println("goroutineA exit") return default: fmt.Println("goroutineA running.") time.Sleep(2 * time.Second) } } }(ctx) go func(ctx context.Context) { for { select { //使用select調用<-ctx.Done()判斷是否要結束 case <-ctx.Done(): fmt.Println("goroutineB exit") return default: fmt.Println("goroutineB running.") time.Sleep(2 * time.Second) } } }(ctx) go func(ctxTwo context.Context) { for { select { //使用select調用<-ctx.Done()判斷是否要結束 case <-ctxTwo.Done(): fmt.Println("goroutineC exit") return default: fmt.Println("goroutineC running.") time.Sleep(2 * time.Second) } } }(ctxTwo) time.Sleep(4 * time.Second) fmt.Println("main fun exit") //取消context cancel() cancelTwo() time.Sleep(5 * time.Second) }
結果:
goroutineA running.
goroutineB running.
goroutineC running.
goroutineB running.
goroutineC running.
goroutineA running.
goroutineC running.
goroutineA running.
goroutineB running.
main fun exit
goroutineC exit
goroutineA exit
goroutineB exit
context還適用於更復雜的場景,如主動取消goroutine或goroutine定時取消等。context接口除了func WithCancel(parent Context) (ctx Context, cancel CancelFunc),還有衍生如下方法
此函數相似於 context.WithDeadline。不一樣之處在於它將持續時間做爲參數輸入而不是時間對象。此函數返回派生 context,若是調用取消函數或超出超時持續時間,則會取消該派生 context。
此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並經過 context 樹與 context 一塊兒傳遞。這意味着一旦得到帶有值的 context,從中派生的任何 context 都會得到此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。
有興趣的同窗請閱讀:https://studygolang.com/pkgdoc
https://tutorialedge.net/gola...
http://goinbigdata.com/golang...
https://medium.com/code-zen/c...
https://blog.csdn.net/u013029...