golang 的提供的 channel 機制是基於 CSP(Communicating Sequencial Processes)模型的併發模式.golang
經過 channel, 能夠很方便的寫出多個 協程 (goroutine)之間協做的代碼, 將順序的代碼改爲並行的代碼很是簡單. 改形成並行的代碼以後, 雖然能夠更好的利用多核的硬件, 有效的提升代碼的執行效率, 可是, 也帶來了代碼控制的問題.安全
並行的代碼顯然比順序執行的代碼更難於管理和控制, 這時, 就得靠 golang 提供的 Context 接口來幫助咱們控制 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 接口來控制併發的協程.code
取消控制指的是任務的發起者, 在特定條件下, 發送信號指示已經接受到任務的協程中止任務的執行.協程
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 個任務以後中止退出.ci
除了取消控制, context 還有超時的控制.it
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 之間經過 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 是線程安全的, 能夠放心的在多個協程中使用.