前言
提到Go語言的併發,就不得不提goroutine,其作爲Go語言的一大特色,在日常開發中使用很多。
在日常使用場景就會涉及一個goroutine啓動或結束,啓動一個goroutine很簡單只需要前面加關鍵詞go即可,而由於每個goroutine都是獨立運行的,其退出有自身決定的,除非main主程序結束或程序崩潰的情況發生。
那麼,如何控制goroutine或者說通知goroutine結束運行呢?
解決的方式其實很簡單,那就是想辦法和goroutine通訊,通知goroutine什麼時候結束,goroutine結束也可以通知其他goroutine或main主程序。
併發控制方法主要有:
全局變量
channel
WaitGroup
context
全局變量
這是併發控制最簡單的實現方式
1、聲明一個全局變量。
2、所有子goroutine共享這個變量,並不斷輪詢這個變量檢查是否有更新;
3、在主進程中變更該全局變量;
4、子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
channel是goroutine之間主要的通訊方式,一般會和select搭配使用。
如想了解channel實現原理可參考
https://github.com/guyan0319/...
1、聲明一個stop
的chan。
2、在goroutine中,使用select判斷stop
是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果沒有接收到,就會執行default
裏邏輯。直到收到stop
的通知。
3、主程序發送了stop<- true
結束的指令後。
4、子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 的場景。
WaitGroup
Go語言提供同步包(sync),源碼(src/sync/waitgroup.go).
Sync包同步提供基本的同步原語,如互斥鎖。除了Once和WaitGroup類型之外,大多數類型都是供低級庫例程使用的。通過Channel和溝通可以更好地完成更高級別的同步。並且此包中的值在使用過後不要拷貝。
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併發控制方式比較輕巧。
注意:
1、計數器不能爲負值
2、WaitGroup對象不是一個引用類型
Context
應用場景:在 Go http 包的 Server 中,每個Request都需要開啓一個goroutine做一些事情,這些goroutine又可能會開啓其他的goroutine。所以我們需要一種可以跟蹤goroutine的方案,纔可以達到控制他們的目的,這就是Go語言爲我們提供的Context,稱之爲上下文。
控制併發的實現方式:
1、 context.Background():返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。
2、context.WithCancel(context.Background()),創建一個可取消的子Context,然後當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。
3、在goroutine中,使用select調用<-ctx.Done()
判斷是否要結束,如果接受到值的話,就可以返回結束goroutine了;如果接收不到,就會繼續進行監控。
4、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),還有衍生以下方法
- func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):
此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您可以創建一個將在以後的某個時間自動取消的 context,並在子函數中傳遞它。當因爲截止日期耗盡而取消該 context 時,獲此 context 的所有函數都會收到通知去停止運行並返回。 - func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):
此函數類似於 context.WithDeadline。不同之處在於它將持續時間作爲參數輸入而不是時間對象。此函數返回派生 context,如果調用取消函數或超出超時持續時間,則會取消該派生 context。
- func WithValue(parent Context, key, val interface{}) 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...