goroutine併發控制
通訊
共享記憶體
func Test() {
ordersInfoApp := make([]orderInfoApp, 0, totalCount)
var mux sync.Mutex
wg := sync.WaitGroup{}
for i := 0; i <= 10; i++ {
wg.Add(1)
go func(pageIndex int) {
// do somethine
var ordersInfo orderInfoApp
mux.Lock()
ordersInfoApp = append(ordersInfoApp, ordersInfo)
mux.Unlock()
wg.Done()
}(i)
}
wg.Wait()
}
一般在簡單的資料傳遞下使用
channel
func Test() {
ordersInfoApp := make([]orderInfoApp, 0, totalCount)
choi := make(chan orderInfoApp, 10)
wg := sync.WaitGroup{}
for i := 0; i <= 10; i++ {
wg.Add(1)
go func(pageIndex int) {
// do somethine
var ordersInfo orderInfoApp
choi <- ordersInfo
wg.Done()
}(i)
}
go func() {
wg.Wait()
close(choi)
}()
for v := range choi {
ordersInfoApp = append(ordersInfoApp, v)
}
}
相對複雜的資料流動情況
同步和控制
goroutine 退出只能由本身控制,不能從外部強制結束該 goroutine 兩種例外情況:main 函式結束或者程式崩潰結束執行
共享變數控制結束
func main() {
running := true
f := func() {
for running {
fmt.Println("i am running")
time.Sleep(1 * time.Second)
}
fmt.Println("exit")
}
go f()
go f()
go f()
time.Sleep(2 * time.Second)
running = false
time.Sleep(3 * time.Second)
fmt.Println("main exit")
}
優點: 實現簡單,不抽象,方便,一個變數即可簡單控制子 goroutine 的進行。 缺點: 結構只能是多讀一寫,不能適應結構複雜的設計,如果有多寫,會出現資料同步問題,需要加鎖或者使用 sync.atomic 不適合用於同級的子 go 程間的通訊,全域性變數傳遞的資訊太少 因為是單向通知,主程式無法等待所有子 goroutine 退出 這種方法只適用於非常簡單的邏輯且併發量不太大的場景
sync.Waitgroup 等待結束
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// do something
}()
}
wg.Wait()
}
channel 控制結束
// 可擴充套件到多個work
func main() {
chClose := make(chan struct{})
go func() {
for {
select {
case <-chClose:
return
default:
}
// do something
}
}()
//chClose<-struct{}
close(chClose)
}
注意 channel 的阻塞情況,避免出現死鎖。 通常 channel 只能由傳送者關閉
-
向無緩衝的 channel 寫入資料會導致該 goroutine 阻塞,直到其他 goroutine 從這個 channel 中讀取資料
-
從無緩衝的 channel 讀出資料,如果 channel 中無資料,會導致該 goroutine 阻塞,直到其他 goroutine 向這個 channel 中寫入資料
-
向帶緩衝的且緩衝已滿的 channel 寫入資料會導致該 goroutine 阻塞,直到其他 goroutine 從這個 channel 中讀取資料
-
向帶緩衝的且緩衝未滿的 channel 寫入資料不會導致該 goroutine 阻塞
-
從帶緩衝的 channel 讀出資料,如果 channel 中無資料,會導致該 goroutine 阻塞,直到其他 goroutine 向這個 channel 中寫入資料
-
從帶緩衝的 channel 讀出資料,如果 channel 中有資料,該 goroutine 不會阻塞
// 讀完結束 for { select { case <-ch: default: goto finish } } finish:
-
如果多個 case 同時就緒時,select 會隨機地選擇一個執行
- case 標籤裡向 channel 傳送或接收資料,case 後面的語句在傳送接收成功後才會執行
- nil channel(讀、寫、讀寫)的 case 標籤會被跳過
limitwaitgroup
type limitwaitgroup struct {
sem chan struct{}
wg sync.WaitGroup
}
func New(n int) *limitwaitgroup {
return &limitwaitgroup{
sem: make(chan struct{}, n),
}
}
func (l *limitwaitgroup) Add() {
l.sem <- struct{}{}
l.wg.Add(1)
}
func (l *limitwaitgroup) Done() {
<-l.sem
l.wg.Done()
}
func (l *limitwaitgroup) Wait() {
l.wg.Wait()
}
// 例子
wg := limitwaitgroup.New(6)
for i := 0; i <= 10; i++ {
wg.Add()
go func(index int){
defer wg.Done()
// do something
}(i)
}
wg.Wait()
context
上下文 go 1.7 作為第一個引數在 goroutine 裡傳遞
Context 的介面定義
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline
獲取設定的截止時間(WithDeadline、WithTimeout), 第一個返回值代表截止時間,第二個返回值代表是否設定了截止時間,false 時表示沒有設定截止時間
Done
方法返回一個關閉的只讀的 chan,型別為struct{}
,在 goroutine 中,如果該方法返回的 chan 可以讀取,則意味著 parent context 已經發起了取消請求,我們通過Done
方法收到這個訊號後,就應該做清理操作,然後退出 goroutine,釋放資源。
Err
context 沒有被結束,返回 nil;已被結束,返回結束的原因(被取消、超時)。
Value
方法通過一個 Key 獲取該 Context 上繫結的值,訪問這個值是執行緒安全的。key 一般定義當前包的一個新的未匯出型別的變數(最好不要使用內建型別),避免和其他 goroutine 的 key 衝突。
- Context 衍生
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
這四個With
函式,接收的都有一個 partent 引數,就是父 Context,我們要基於這個父 Context 建立出子 Context 的意思,這種方式可以理解為子 Context 對父 Context 的繼承,也可以理解為基於父 Context 的衍生。
通過這些函式,就建立了一顆 Context 樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。
WithCancel
函式,傳遞一個父 Context 作為引數,返回子 Context,以及一個取消函式用來取消 Context。 WithDeadline
函式,和WithCancel
差不多,它會多傳遞一個截止時間引數,意味著到了這個時間點,會自動取消 Context,也可以不等到這個時候,可以提前通過取消函式進行取消。
WithTimeout
和WithDeadline
基本上一樣,這個表示是超時自動取消,設定多少時間後自動取消 Context。
WithValue
函式和取消 Context 無關,生成一個繫結了一個鍵值對資料的 Context,這個繫結的資料可以通過Context.Value
方法訪問到
- 例子
WithCancel
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "goroutine 1")
go watch(ctx, "goroutine 2")
go watch(ctx, "goroutine 3")
time.Sleep(10 * time.Second)
fmt.Println("開始結束goroutine")
cancel()
time.Sleep(5 * time.Second)
fmt.Println(ctx.Err())
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "over")
return
default:
fmt.Println(name, "running")
time.Sleep(2 * time.Second)
}
}
}
// output:
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 2 running
goroutine 3 running
goroutine 1 running
goroutine 3 running
goroutine 2 running
goroutine 1 running
開始結束goroutine
goroutine 1 over
goroutine 2 over
goroutine 3 over
context canceled
WithDeadline
func main() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
go watch(ctx, "goroutine 1")
go watch(ctx, "goroutine 2")
go watch(ctx, "goroutine 3")
_ = cancel
time.Sleep(8 * time.Second)
fmt.Println(ctx.Err())
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "over")
return
default:
fmt.Println(name, "running")
time.Sleep(2 * time.Second)
}
}
}
// output:
goroutine 3 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 2 running
goroutine 3 over
goroutine 1 over
goroutine 2 over
context deadline exceeded
WithTimeout
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
go watch(ctx, "goroutine 1")
go watch(ctx, "goroutine 2")
go watch(ctx, "goroutine 3")
_ = cancel
time.Sleep(8 * time.Second)
fmt.Println(ctx.Err())
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "over")
return
default:
fmt.Println(name, "running")
time.Sleep(2 * time.Second)
}
}
}
// output:
goroutine 3 running
goroutine 1 running
goroutine 2 running
goroutine 3 running
goroutine 2 running
goroutine 1 running
goroutine 2 running
goroutine 1 running
goroutine 3 running
goroutine 2 over
goroutine 3 over
goroutine 1 over
context deadline exceeded
WithValue
type key int // 未匯出的包私有型別
var kkk key = 0
func main() {
ctx, cancel := context.WithCancel(context.Background())
// WithValue是沒有取消函式的
ctx = context.WithValue(ctx, kkk, "100W")
go watch(ctx, "goroutine 1")
go watch(ctx, "goroutine 2")
go watch(ctx, "goroutine 3")
time.Sleep(8 * time.Second)
fmt.Println("開始結束goroutine")
cancel()
time.Sleep(3 * time.Second)
fmt.Println(ctx.Err())
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "over")
return
default:
fmt.Println(name, "running", "爸爸給我了", ctx.Value(kkk).(string))
time.Sleep(2 * time.Second)
}
}
}
// output:
goroutine 1 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 1 running 爸爸給我了 100W
goroutine 3 running 爸爸給我了 100W
goroutine 2 running 爸爸給我了 100W
開始結束goroutine
goroutine 2 over
goroutine 3 over
goroutine 1 running 爸爸給我了 100W
goroutine 1 over
context canceled
控制多個 goroutine
func main() {
http.HandleFunc("/", func(W http.ResponseWriter, r *http.Request) {
fmt.Println("收到請求")
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, 2)
go worker(ctx, 3)
time.Sleep(time.Second * 10)
cancel()
fmt.Println(ctx.Err())
})
http.ListenAndServe(":9290", nil)
}
func worker(ctx context.Context, speed int) {
reader := func(n int) {
for {
select {
case <-ctx.Done():
return
default:
break
}
fmt.Println("reader:", n)
time.Sleep(time.Duration(n) * time.Second)
}
}
go reader(2)
go reader(1)
for {
select {
case <-ctx.Done():
return
default:
break
}
fmt.Println("worker:", speed)
time.Sleep(time.Duration(speed) * time.Second)
}
}
// output:
收到請求
worker: 2
reader: 1
worker: 3
reader: 1
reader: 2
reader: 2
reader: 1
reader: 1
reader: 1
reader: 2
worker: 2
reader: 2
reader: 1
reader: 1
reader: 1
worker: 3
reader: 1
worker: 2
reader: 2
reader: 1
reader: 2
reader: 1
context canceled
-
使用規則 使用 Context 的程式應遵循以下這些規則來保持跨包介面的一致和方便靜態分析工具(go vet)來檢查上下文傳播是否有潛在問題。
- 不要將 Context 儲存在結構型別中,而是顯式的傳遞給每個需要的函式; Context 應該作為函式的第一個引數傳遞,通常命名為 ctx:
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
-
即使函式可以接受 nil 值,也不要傳遞 nil Context。如果不確定要使用哪個 Context,請傳遞 context.TODO。
-
使用 context 的 Value 相關方法只應該用於在程式和介面中傳遞和請求相關的後設資料,不要用它來傳遞一些可選的引數
- 相同的 Context 可以傳遞給在不同 goroutine 中執行的函式; Context 對於多個 goroutine 同時使用是安全的。
相關文章
- go併發之goroutine和channel,併發控制入門篇Go
- 「Golang成長之路」併發之GoroutineGolang
- 說說Golang goroutine併發那些事兒Golang
- Go:21---goroutine併發執行體Go
- Golang併發程式設計——goroutine、channel、syncGolang程式設計
- Go併發程式設計--正確使用goroutineGo程式設計
- Go 併發程式設計 - Goroutine 基礎 (一)Go程式設計
- 【Go併發程式設計】Goroutine的基本使用Go程式設計
- 併發控制
- Guava併發:使用Monitor控制併發Guava
- mysql併發控制MySql
- PGSQL併發控制SQL
- oracle併發控制Oracle
- Go語言 | CSP併發模型與Goroutine的基本使用Go模型
- go中控制goroutine數量Go
- Golang語言goroutine協程併發安全及鎖機制Golang
- 併發程式設計之:JUC併發控制工具程式設計
- goroutine併發執行多個任務並依次返回結果Go
- 學習之路 / goroutine 併發協程池設計及實現Go
- PostgreSQL 併發控制機制(3):基於時間戳的併發控制SQL時間戳
- 【資料庫】併發控制資料庫
- Golang併發程式設計優勢與核心goroutine及注意細節Golang程式設計
- 更簡的併發程式碼,更強的併發控制
- MySQL如何加鎖控制併發MySql
- pgsql事務與併發控制SQL
- InnoDB -MVCC多版本併發控制MVC
- MySQL MVCC(多版本併發控制)MySqlMVC
- oracle併發與多版本控制Oracle
- 15-併發控制理論
- 5分鐘搞定Promise控制併發Promise
- MVCC多版本併發控制器MVC
- 淺談java中的併發控制Java
- mysql併發控制原理知識點MySql
- 3.1 JDK併發包之同步控制JDK
- Java併發控制機制詳解Java
- mysql的mvcc(多版本併發控制)MySqlMVC
- 資料複製的併發控制
- 【C/C++】5.併發控制C++