- 概要
- goroutine 的控制
- 取消控制
- 超時控制
- goroutine 之間的傳值
- 總結
概要
golang 的提供的 channel 機制是基于 CSP(Communicating Sequencial Processes)模型的并發模式.
通過 channel, 可以很方便的寫出多個 協程 (goroutine)之間協作的代碼, 將順序的代碼改成并行的代碼非常簡單. 改造成并行的代碼之后, 雖然可以更好的利用多核的硬體, 有效的提高代碼的執行效率, 但是, 也帶來了代碼控制的問題.
并行的代碼顯然比順序執行的代碼更難于管理和控制, 這時, 就得靠 golang 提供的 Context 介面來幫助我們控制 goroutine 了.
goroutine 的控制
goroutine 之間最基本的控制, 就是通過 channel 來互動資料:
1 func routineSample() {
2 ch := make(chan int, 10)
3 go p1(ch)
4 go c1(ch)
5 go c2(ch)
6
7 time.Sleep(10 * time.Second)
8 }
9
10 func p1(ch chan int) {
11 fmt.Println("Parent go routine!")
12
13 for i := 0; i < 10; i++ {
14 ch <- i
15 }
16
17 close(ch)
18 }
19
20 func c1(ch chan int) {
21 fmt.Println("Child 1 go routine!")
22 for c := range ch {
23 fmt.Printf("child 1, recivie: %d\n", c)
24 time.Sleep(100 * time.Millisecond)
25 }
26 }
27
28 func c2(ch chan int) {
29 fmt.Println("Child 2 go routine!")
30 for c := range ch {
31 fmt.Printf("child 2, recivie: %d\n", c)
32 time.Sleep(100 * time.Millisecond)
33 }
34 }
上述是最基本的示例, p1 函式不斷向 channel 中發送資料, c1 和 c2 負責處理資料. 雖然通過 channel 實作 c1, c2 的并發很簡單, 但是可以看出, p1 要想控制 c1, c2 沒有那么容易.
這時, 就需要通過 Context 介面來控制并發的協程.
取消控制
取消控制指的是任務的發起者, 在特定條件下, 發送信號指示已經接受到任務的協程停止任務的執行.
1 func routineSample() {
2 ch := make(chan int, 10)
3 ctx, cancel := context.WithCancel(context.Background())
4 go p1(ctx, ch)
5 go c1(ctx, ch)
6 go c2(ctx, ch)
7
8 // 300 ms之后取消任務
9 time.Sleep(300 * time.Millisecond)
10 cancel()
11
12 time.Sleep(10 * time.Second)
13 }
14
15 func p1(ctx context.Context, ch chan int) {
16 fmt.Println("Parent go routine!")
17
18 for i := 0; i < 10; i++ {
19 ch <- i
20 }
21
22 close(ch)
23 }
24
25 func c1(ctx context.Context, ch chan int) {
26 fmt.Println("Child 1 go routine!")
27 for c := range ch {
28 select {
29 case <-ctx.Done():
30 fmt.Println("child 1, return!")
31 return
32 default:
33 fmt.Printf("child 1, recivie: %d\n", c)
34 }
35 time.Sleep(100 * time.Millisecond)
36 }
37 }
38
39 func c2(ctx context.Context, ch chan int) {
40 fmt.Println("Child 2 go routine!")
41 for c := range ch {
42 select {
43 case <-ctx.Done():
44 fmt.Println("child 2, return!")
45 return
46 default:
47 fmt.Printf("child 2, recivie: %d\n", c)
48 }
49 time.Sleep(100 * time.Millisecond)
50 }
51 }
300 毫秒后, 發送取消任務的信號 cancel() , c1 和 c2 通過 select 判斷是否有取消信號, 收到取消信號后, 退出處理.
通過執行結果可以看出, c1 和 c2 大約處理 5~6 個任務之后停止退出.
超時控制
除了取消控制, context 還有超時的控制.
1 func routineSample() {
2 ch := make(chan int, 10)
3 ctx, _ := context.WithTimeout(context.Background(), 300*time.Millisecond)
4 go p1(ctx, ch)
5 go c1(ctx, ch)
6 go c2(ctx, ch)
7
8 time.Sleep(10 * time.Second)
9 }
10
11 func p1(ctx context.Context, ch chan int) {
12 fmt.Println("Parent go routine!")
13
14 for i := 0; i < 10; i++ {
15 ch <- i
16 }
17
18 close(ch)
19 }
20
21 func c1(ctx context.Context, ch chan int) {
22 fmt.Println("Child 1 go routine!")
23 for c := range ch {
24 select {
25 case <-ctx.Done():
26 fmt.Println("child 1, return!")
27 return
28 default:
29 fmt.Printf("child 1, recivie: %d\n", c)
30 }
31 time.Sleep(100 * time.Millisecond)
32 }
33 }
34
35 func c2(ctx context.Context, ch chan int) {
36 fmt.Println("Child 2 go routine!")
37 for c := range ch {
38 select {
39 case <-ctx.Done():
40 fmt.Println("child 2, return!")
41 return
42 default:
43 fmt.Printf("child 2, recivie: %d\n", c)
44 }
45 time.Sleep(100 * time.Millisecond)
46 }
47 }
控制超時的 WithTimeout 也回傳一個 cancel 函式, 可以在超時到達之前來取消任務的執行, 上面的例子等待超時時間達到后自動取消任務, 沒有使用 cancel 函式.
goroutine 之間的傳值
一般來說, goroutine 之間通過 channel 傳遞都是業務資料, 除此之外, 還可以通過 channel 來傳遞一些控制 goroutine 的元資料.
1 func routineSample() {
2 ch := make(chan int, 10)
3 // 哪個goroutine收到5號任務, 就退出, 不再做后續的任務
4 ctx := context.WithValue(context.Background(), "finish", 5)
5 go p1(ctx, ch)
6 go c1(ctx, ch)
7 go c2(ctx, ch)
8
9 time.Sleep(10 * time.Second)
10 }
11
12 func p1(ctx context.Context, ch chan int) {
13 fmt.Println("Parent go routine!")
14
15 for i := 0; i < 10; i++ {
16 ch <- i
17 }
18
19 close(ch)
20 }
21
22 func c1(ctx context.Context, ch chan int) {
23 fmt.Println("Child 1 go routine!")
24 for c := range ch {
25 if c == ctx.Value("finish").(int) {
26 fmt.Println("child 1, return!")
27 return
28 }
29 fmt.Printf("child 1, recivie: %d\n", c)
30 time.Sleep(100 * time.Millisecond)
31 }
32 }
33
34 func c2(ctx context.Context, ch chan int) {
35 fmt.Println("Child 2 go routine!")
36 for c := range ch {
37 if c == ctx.Value("finish").(int) {
38 fmt.Println("child 2, return!")
39 return
40 }
41 fmt.Printf("child 2, recivie: %d\n", c)
42 time.Sleep(100 * time.Millisecond)
43 }
44 }
上面的例子是在 context 中放置一個 key="finish" 的任務號, 如果 c1 或者 c2 收到的任務號和它相同, 則退出任務的執行. 通過運行上面的例子可以看出, c1 或者 c2 執行到 5 號任務的時候就會退出協程. 但是誰收到 5 號任務是不確定的, 多執行幾次上面的代碼, 可以發現有時是 c1 退出, 有時是 c2 退出.
總結
context 是控制并發協程的背景關系, 利用 context, 可以大量簡化控制協程的超時, 取消協程執行, 以及協程之間傳值的代碼. context 很方便, 但也不能亂用, 通過 channel 傳遞的業務資料, 不要放在 context 中來傳遞.
此外, context 是執行緒安全的, 可以放心的在多個協程中使用.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/47184.html
標籤:Go
上一篇:Go 驗證是否字串包含中文
