go context 講解

  控制併發有兩種經典的方式,一種是WaitGroup,另一種就是Context,今天我就談談Context。安全

什麼是WaitGroup

WaitGroup之前咱們在併發的時候介紹過,它是一種控制併發的方式,它的這種方式是控制多個goroutine同時完成。網絡

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("1號完成")
		wg.Done()
	}()
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("2號完成")
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("好了,你們都幹完了,放工")
}

  這是一種控制併發的方式,這種尤爲適用於,好多個goroutine協同作一件事情的時候,由於每一個goroutine作的都是這件事情的一部分,只有所有的goroutine都完成,這件事情纔算是完成,這是等待的方式。一個很簡單的例子,必定要例子中的2個goroutine同時作完,纔算是完成,先作好的就要等着其餘未完成的,全部的goroutine要都所有完成才能夠。多線程

在實際的業務種,咱們可能會有這麼一種場景:須要咱們主動的通知某一個goroutine結束。好比咱們開啓一個後臺goroutine一直作事情,好比監控,如今不須要了,就須要通知這個監控goroutine結束,否則它會一直跑,就泄漏了。併發

  chan通知

  咱們都知道一個goroutine啓動後,咱們是沒法控制他的,大部分狀況是等待它本身結束,那麼若是這個goroutine是一個不會本身結束的後臺goroutine呢?好比監控等,會一直運行的。函數

這種狀況化,一直傻瓜式的辦法是全局變量,其餘地方經過修改這個變量完成結束通知,而後後臺goroutine不停的檢查這個變量,若是發現被通知關閉了,就自我結束。測試

這種方式也能夠,可是首先咱們要保證這個變量在多線程下的安全,基於此,有一種更好的方式:chan + select 。網站

func main() {
	stop := make(chan bool)
	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("監控退出,中止了...")
				return
			default:
				fmt.Println("goroutine監控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}()
	time.Sleep(10 * time.Second)
	fmt.Println("能夠了,通知監控中止")
	stop<- true
	//爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了
	time.Sleep(5 * time.Second)
}

  有了以上的邏輯,咱們就能夠在其餘goroutine種,給stop chan發送值了,例子中是在main goroutine中發送的,控制讓這個監控的goroutine結束。例子中咱們定義一個stop的chan,通知他結束後臺goroutine。實現也很是簡單,在後臺goroutine中,使用select判斷stop是否能夠接收到值,若是能夠接收到,就表示能夠退出中止了;若是沒有接收到,就會執行default裏的監控邏輯,繼續監控,只到收到stop的通知。spa

  發送了stop<- true結束的指令後,我這裏使用time.Sleep(5 * time.Second)故意停頓5秒來檢測咱們結束監控goroutine是否成功。若是成功的話,不會再有goroutine監控中...的輸出了;若是沒有成功,監控goroutine就會繼續打印goroutine監控中...輸出。線程

  這種chan+select的方式,是比較優雅的結束一個goroutine的方式,不過這種方式也有侷限性,若是有不少goroutine都須要控制結束怎麼辦呢?若是這些goroutine又衍生了其餘更多的goroutine怎麼辦呢?若是一層層的無窮盡的goroutine呢?這就很是複雜了,即便咱們定義不少chan也很難解決這個問題,由於goroutine的關係鏈就致使了這種場景很是複雜。code

  初識Context

  上面說的這種場景是存在的,好比一個網絡請求Request,每一個Request都須要開啓一個goroutine作一些事情,這些goroutine又可能會開啓其餘的goroutine。因此咱們須要一種能夠跟蹤goroutine的方案,才能夠達到控制他們的目的,這就是Go語言爲咱們提供的Context,稱之爲上下文很是貼切,它就是goroutine的上下文。

下面咱們就使用Go Context重寫上面的示例。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("監控退出,中止了...")
				return
			default:
				fmt.Println("goroutine監控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)
	time.Sleep(10 * time.Second)
	fmt.Println("能夠了,通知監控中止")
	cancel()
	//爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了
	time.Sleep(5 * time.Second)
}

  

context.Background() 返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。而後咱們使用context.WithCancel(parent)函數,建立一個可取消的子Context,而後看成參數傳給goroutine使用,這樣就可使用這個子Context跟蹤這個goroutine。重寫比較簡單,就是把原來的chan stop 換成Context,使用Context跟蹤goroutine,以便進行控制,好比結束等。

在goroutine中,使用select調用<-ctx.Done()判斷是否要結束,若是接受到值的話,就能夠返回結束goroutine了;若是接收不到,就會繼續進行監控。

那麼是如何發送結束指令的呢?這就是示例中的cancel函數啦,它是咱們調用context.WithCancel(parent)函數生成子Context的時候返回的,第二個返回值就是這個取消函數,它是CancelFunc類型的。咱們調用它就能夠發出取消指令,而後咱們的監控goroutine就會收到信號,就會返回結束。

  Context控制多個goroutine

  使用Context控制一個goroutine的例子如上,很是簡單,下面咱們看看控制多個goroutine的例子,其實也比較簡單。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"【監控1】")
	go watch(ctx,"【監控2】")
	go watch(ctx,"【監控3】")
	time.Sleep(10 * time.Second)
	fmt.Println("能夠了,通知監控中止")
	cancel()
	//爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了
	time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"監控退出,中止了...")
			return
		default:
			fmt.Println(name,"goroutine監控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

  


  《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公衆號
flysnow_org或者網站http://www.flysnow.org/,第一時間看後續筆記。以爲有幫助的話,順手分享到朋友圈吧,感謝支持。示例中啓動了3個監控goroutine進行不斷的監控,每個都使用了Context進行跟蹤,當咱們使用cancel函數通知取消時,這3個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器同樣,按下開關後,全部基於這個Context或者衍生的子Context都會收到通知,這時就能夠進行清理操做了,最終釋放goroutine,這就優雅的解決了goroutine啓動後不可控的問題。

  Context接口

  Context的接口定義的比較簡潔,咱們看下這個接口的方法。

  

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

  Deadline方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,若是須要取消的話,須要調用取消函數進行取消。這個接口共有4個方法,瞭解這些方法的意思很是重要,這樣咱們才能夠更好的使用他們。

Done方法返回一個只讀的chan,類型爲struct{},咱們在goroutine中,若是該方法返回的chan能夠讀取,則意味着parent context已經發起了取消請求,咱們經過Done方法收到這個信號後,就應該作清理操做,而後退出goroutine,釋放資源。

Err方法返回取消的錯誤緣由,由於什麼Context被取消。

Value方法獲取該Context上綁定的值,是一個鍵值對,因此要經過一個Key才能夠獲取對應的值,這個值通常是線程安全的。

以上四個方法中經常使用的就是Done了,若是Context取消的時候,咱們就能夠獲得一個關閉的chan,關閉的chan是能夠讀取的,因此只要能夠讀取的時候,就意味着收到Context取消的信號了,如下是這個方法的經典用法。

func Stream(ctx context.Context, out chan<- Value) error {
	for {
		v, err := DoSomething(ctx)
		if err != nil {
			return err
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case out <- v:
		}
	}
}

    

 
 

Context接口並不須要咱們實現,Go內置已經幫咱們實現了2個,咱們代碼中最開始都是以這兩個內置的做爲最頂層的partent context,衍生出更多的子Context。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
func Background() Context {
	return background
}
func TODO() Context {
	return todo
}

  一個是TODO,它目前還不知道具體的使用場景,若是咱們不知道該使用什麼Context的時候,可使用這個。一個是Background,主要用於main函數、初始化以及測試代碼中,做爲Context這個樹結構的最頂層的Context,也就是根Context。

他們兩個本質上都是emptyCtx結構體類型,是一個不可取消,沒有設置截止時間,沒有攜帶任何值的Context。

type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}
func (*emptyCtx) Done() <-chan struct{} {
	return nil
}
func (*emptyCtx) Err() error {
	return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

  


Context的繼承衍生

這就是emptyCtx實現Context接口的方法,能夠看到,這些方法什麼都沒作,返回的都是nil或者零值。

有了如上的根Context,那麼是如何衍生更多的子Context的呢?這就要靠context包爲咱們提供的With系列的函數了。

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,固然咱們也能夠不等到這個時候,能夠提早經過取消函數進行取消。

WithTimeoutWithDeadline基本上同樣,這個表示是超時自動取消,是多少時間後自動取消Context的意思。

WithValue函數和取消Context無關,它是爲了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據能夠經過Context.Value方法訪問到,後面咱們會專門講。

你們可能留意到,前三個函數都返回一個取消函數CancelFunc,這是一個函數類型,它的定義很是簡單。

type CancelFunc func()

  

WithValue傳遞元數據這就是取消函數的類型,該函數能夠取消一個Context,以及這個節點Context下全部的全部的Context,無論有多少層級。

經過Context咱們也能夠傳遞一些必須的元數據,這些數據會附加在Context上以供使用。

var key string="name"
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx:=context.WithValue(ctx,key,"【監控1】")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("能夠了,通知監控中止")
	cancel()
	//爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了
	time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//取出值
			fmt.Println(ctx.Value(key),"監控退出,中止了...")
			return
		default:
			//取出值
			fmt.Println(ctx.Value(key),"goroutine監控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

  


咱們可使用context.WithValue方法附加一對K-V的鍵值對,這裏Key必須是等價性的,也就是具備可比性;Value值要是線程安全的。在前面的例子,咱們經過傳遞參數的方式,把name的值傳遞給監控函數。在這個例子裏,咱們實現同樣的效果,可是經過的是Context的Value的方式。

這樣咱們就生成了一個新的Context,這個新的Context帶有這個鍵值對,在使用的時候,能夠經過Value方法讀取ctx.Value(key)

記住,使用WithValue傳值,通常是必須的值,不要什麼值都傳遞。

Context 使用原則

    1. 不要把Context放在結構體中,要以參數的方式傳遞
    2. 以Context做爲參數的函數方法,應該把Context做爲第一個參數,放在第一位。
    3. 給一個函數方法傳遞Context的時候,不要傳遞nil,若是不知道傳遞什麼,就使用context.TODO
    4. Context的Value相關方法應該傳遞必須的數據,不要什麼數據都使用這個傳遞
    5. Context是縣城安全的,能夠放心的在多個goroutine中傳遞
相關文章
相關標籤/搜索