1. 程式人生 > 其它 >Go通關11:併發控制神器之Context深入淺出

Go通關11:併發控制神器之Context深入淺出

協程如何退出

一個協程啟動後,一般是程式碼執行完畢,自動退出,但是如果需要提前終止怎麼辦呢?
一個辦法是定義一個全域性變數,協程中通過檢查這個變數的變化來決定是否退出。這種辦法須要加鎖來保證併發安全,說到這裡,有沒有想的什麼解決方案?
select + channel 來實現:

package main
import (
	"fmt"
	"sync"
	"time"
)
func main() {
	var wg sync.WaitGroup
	stopWk := make(chan bool)
	wg.Add(1)
	go func() {
		defer wg.Done()
		worker(stopWk)
	}()
	time.Sleep(3*time.Second) //工作3秒
	stopWk <- true //3秒後發出停止指令
	wg.Wait()
}

func worker(stopWk chan bool){
	for {
		select {
		case <- stopWk:
			fmt.Println("下班咯~~~")
			return
		default:
			fmt.Println("認真摸魚中,請勿打擾...")
		}
		time.Sleep(1*time.Second)
	}
}

執行結果:

認真摸魚中,請勿打擾...
認真摸魚中,請勿打擾...
認真摸魚中,請勿打擾...
下班咯~~~

可以看到,每秒列印一次“認真摸魚中,請勿打擾...”,3秒後發出停止指令,程式進入 “下班咯~~~”。

Context 初體驗

上面我們使用 select+channel 來實現了協程的終止,但是如果我們想要同時取消多個協程怎麼辦呢?如果需要定時取消又怎麼辦呢?
此時,Context 就需要登場了,它可以跟蹤每個協程,我們重寫上面的示例:

package main
import (
	"context"
	"fmt"
	"sync"
	"time"
)
func main() {
	var wg sync.WaitGroup
	ctx, stop := context.WithCancel(context.Background())
	wg.Add(1)
	go func() {
		defer wg.Done()
		worker(ctx)
	}()
	time.Sleep(3*time.Second) //工作3秒
	stop() //3秒後發出停止指令
	wg.Wait()
}

func worker(ctx context.Context){
	for {
		select {
		case <- ctx.Done():
			fmt.Println("下班咯~~~")
			return
		default:
			fmt.Println("認真摸魚中,請勿打擾...")
		}
		time.Sleep(1*time.Second)
	}
}

執行結果:

認真摸魚中,請勿打擾...
認真摸魚中,請勿打擾...
認真摸魚中,請勿打擾...
下班咯~~~

Context 介紹

Context 是併發安全的,它是一個介面,可以手動、定時、超時發出取消訊號、傳值等功能,主要是用於控制多個協程之間的協作、取消操作。

Context 介面有四個方法:

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}
  • Deadline 方法:可以獲取設定的截止時間,返回值 deadline 是截止時間,到了這個時間,Context 會自動發起取消請求,返回值 ok 表示是否設定了截止時間。
  • Done 方法:返回一個只讀的 channel ,型別為 struct{}。如果這個 chan 可以讀取,說明已經發出了取消訊號,可以做清理操作,然後退出協程,釋放資源。
  • Err 方法:返回Context 被取消的原因。
  • Value 方法:獲取 Context 上繫結的值,是一個鍵值對,通過 key 來獲取對應的值。

最常用的是 Done 方法,在 Context 取消的時候,會關閉這個只讀的 Channel,相當於發出了取消訊號。

Context 樹

我們並不需要自己去實現 Context 介面,Go 語言提供了函式來生成不同的 Context,通過這些函式可以生成一顆 Context 樹,這樣 Context 就可以關聯起來,父級 Context 發出取消訊號,子級 Context 也會發出,這樣就可以控制不同層級的協程退出。

生成根節點

  1. emptyCtx是一個int型別的變數,但實現了context的介面。emptyCtx沒有超時時間,不能取消,也不能儲存任何額外資訊,所以emptyCtx用來作為 context 樹的根節點。
  2. 但是我們一般不直接使用emptyCtx,而是使用由emptyCtx例項化的兩個變數(background 、todo),分別通過呼叫BackgroundTODO方法得到,但這兩個 context 在實現上是一樣的。

Background和TODO方法區別:
BackgroundTODO只是用於不同場景下:Background通常被用於主函式、初始化以及測試中,作為一個頂層的context,也就是說一般我們建立的context都是基於Background;而TODO是在不確定使用什麼context的時候才會使用。

生成樹的函式

  1. 可以通過 context。Background() 獲取一個根節點 Context。
  2. 有了根節點後,再使用以下四個函式來生成 Context 樹:
  • WithCancel(parent Context):生成一個可取消的 Context。
  • WithDeadline(parent Context, d time.Time):生成一個可定時取消的 Context,引數 d 為定時取消的具體時間。
  • WithTimeout(parent Context, timeout time.Duration):生成一個可超時取消的 Context,引數 timeout 用於設定多久後取消
  • WithValue(parent Context, key, val interface{}):生成一個可攜帶 key-value 鍵值對的 Context。

Context 取消多個協程

如果一個 Context 有子 Context,在該 Context 取消時,其下的所有子 Context 都會被取消。

Context 傳值

Context 不僅可以發出取消訊號,還可以傳值,可以把它儲存的值提供其他協程使用。

示例:

package main
import (
	"context"
	"fmt"
	"sync"
	"time"
)
func main() {
	var wg sync.WaitGroup
	ctx, stop := context.WithCancel(context.Background())
	valCtx := context.WithValue(ctx, "position","gopher")
	wg.Add(2)
	go func() {
		defer wg.Done()
		worker(valCtx, "打工人1")
	}()
	go func() {
		defer wg.Done()
		worker(valCtx, "打工人2")
	}()
	time.Sleep(3*time.Second) //工作3秒
	stop() //3秒後發出停止指令
	wg.Wait()
}

func worker(valCtx context.Context, name string){
	for {
		select {
		case <- valCtx.Done():
			fmt.Println("下班咯~~~")
			return
		default:
			position := valCtx.Value("position")
			fmt.Println(name,position, "認真摸魚中,請勿打擾...")
		}
		time.Sleep(1*time.Second)
	}
}

執行結果:

打工人2 gopher 認真摸魚中,請勿打擾...
打工人1 gopher 認真摸魚中,請勿打擾...
打工人1 gopher 認真摸魚中,請勿打擾...
打工人2 gopher 認真摸魚中,請勿打擾...
打工人2 gopher 認真摸魚中,請勿打擾...
打工人1 gopher 認真摸魚中,請勿打擾...
下班咯~~~
下班咯~~~

Context 使用原則

  • Context 不要放在結構體中,需要以引數方式傳遞
  • Context 作為函式引數時,要放在第一位,作為第一個引數
  • 使用 context。Background 函式生成根節點的 Context
  • Context 要傳值必要的值,不要什麼都傳
  • Context 是多協程安全的,可以在多個協程中使用