關於Go Context

參考:https://studygolang.com/articles/12566

package main

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"strings"
)

//這個程序的消息總線
var MessageBus = make(chan string)

type CtxWithCancel struct {
	Ctx      context.Context
	CtxId    string
	CancFunc context.CancelFunc
	SonLeft  *CtxWithCancel
	SonRight *CtxWithCancel
}

//創建樹形的Context結構。每個節點都有左右兩個葉子節點,在創建葉子節點的同時,將葉子節點與一個協程綁定,用以控制這個協程
func buildCtxTree(parentCtx *CtxWithCancel, record map[string]*CtxWithCancel) {
	if len(strings.Split(parentCtx.CtxId, ".")) >= 4 {
		return
	}
	//context.WithCancel從parentCtx.Ctx創建了一個子context,同時返回了一個CancelFunc,這個子節點受這個CancelFunc控制
	leftCtx, leftCanc := context.WithCancel(parentCtx.Ctx)
	parentCtx.SonLeft = &CtxWithCancel{leftCtx, parentCtx.CtxId + ".0", leftCanc, nil, nil}
	rightCtx, rightCanc := context.WithCancel(parentCtx.Ctx)
	parentCtx.SonRight = &CtxWithCancel{rightCtx, parentCtx.CtxId + ".1", rightCanc, nil, nil}
	fmt.Println("build success-->" + parentCtx.CtxId + "-->" + parentCtx.SonLeft.CtxId + "-->" + parentCtx.SonRight.CtxId)
	//記錄,方便查找
	record[parentCtx.SonLeft.CtxId] = parentCtx.SonLeft
	record[parentCtx.SonRight.CtxId] = parentCtx.SonRight
	//將葉子節點與協程綁定,用以控制這個協程
	go routineTask(parentCtx.SonLeft.CtxId+"_task", parentCtx.SonLeft.Ctx)
	go routineTask(parentCtx.SonRight.CtxId+"_task", parentCtx.SonRight.Ctx)
	buildCtxTree(parentCtx.SonLeft, record)
	buildCtxTree(parentCtx.SonRight, record)
}

func routineTask(taskName string, ctx context.Context) {
	fmt.Println("start task-->", taskName)
	for {
		select {
		case msg := <-MessageBus:
			fmt.Println("print-->", taskName, "-->", msg)
		//當ctx對應的CancelFunc函數被調用時,這個Done()消息會到達
		case <-ctx.Done():
			fmt.Println(taskName + "-->exit")
			return
		}
	}
}

func main() {
	//context.Background函數的返回值是一個空的context,經常作爲樹的根結點。它一般由接收請求的第一個routine創建,不能被取消、沒有值、也沒有過期時間。
	rootCtx := context.Background()
	rootCtxWithCanc := CtxWithCancel{rootCtx, "0", nil, nil, nil}
	ctxMap := make(map[string]*CtxWithCancel)
	//記錄,方便查找
	ctxMap["0"] = &rootCtxWithCanc
	buildCtxTree(&rootCtxWithCanc, ctxMap)

	//如果輸入的字符串不是context id,那麼觸發消息總線上的通信,由某個協程競爭打印消息;
	//如果輸入的字符串是context id,那麼對應的context及其子孫context控制的協程退出
	inputReader := bufio.NewReader(os.Stdin)
	for {
		str, err := inputReader.ReadString('\n')
		str = str[:len(str)-1]
		if err != nil {
			return
		}
		ctx := ctxMap[str]
		if ctx != nil {
			//根Context沒有CancelFunc函數
			if strings.EqualFold(ctx.CtxId, "0") {
				ctx.SonLeft.CancFunc()
				ctx.SonRight.CancFunc()
			} else {
				ctx.CancFunc()
			}
		} else {
			MessageBus <- str
		}
	}
}

輸出結果如下(其中紅色部分爲輸入)

GOROOT=/home/yong/Desktop/env_init/go_home/go #gosetup
GOPATH=/mnt/hgfs/go-env-1/go-path:/home/yong/Desktop/env_init/go_home/go-path #gosetup
/home/yong/Desktop/env_init/go_home/go/bin/go build -o /tmp/___go_build_main_go /home/yong/go/src/test1/main.go #gosetup
/tmp/___go_build_main_go #gosetup
build success-->0-->0.0-->0.1
build success-->0.0-->0.0.0-->0.0.1
build success-->0.0.0-->0.0.0.0-->0.0.0.1
build success-->0.0.1-->0.0.1.0-->0.0.1.1
build success-->0.1-->0.1.0-->0.1.1
build success-->0.1.0-->0.1.0.0-->0.1.0.1
build success-->0.1.1-->0.1.1.0-->0.1.1.1
start task--> 0.0.0.1_task
start task--> 0.1_task
start task--> 0.0.0_task
start task--> 0.0.1_task
start task--> 0.0.0.0_task
start task--> 0.0.1.1_task
start task--> 0.0.1.0_task
start task--> 0.1.1.1_task
start task--> 0.1.0.0_task
start task--> 0.1.0.1_task
start task--> 0.1.1.0_task
start task--> 0.1.0_task
start task--> 0.0_task
start task--> 0.1.1_task
aaa
print--> 0.0.0.1_task --> aaa
bbb
print--> 0.0.0_task --> bbb
ccc
print--> 0.0.1_task --> ccc
0.1.0
0.1.0_task-->exit
0.1.0.0_task-->exit
0.1.0.1_task-->exit
0.0
0.0.0.0_task-->exit
0.0_task-->exit
0.0.0_task-->exit
0.0.0.1_task-->exit
0.0.1_task-->exit
0.0.1.1_task-->exit
0.0.1.0_task-->exit
0
0.1.1_task-->exit
0.1_task-->exit
0.1.1.1_task-->exit
0.1.1.0_task-->exit
^C
Process finished with exit code 2

Go Context的主要作用是控制協程,防止協程泄露。(關於協程泄露,見《Go語言第十一課 併發(三)Channel緩存與阻塞》)

一般而言,父級協程比較清楚子級協程應該在何時退出。所以往往需要子級協程將控制權交給父級協程,從而在父級協程認爲不再需要的時候控制退出子級協程。

舉個例子,在kafka(sarama庫)消費者組消費消息時(代碼位於 https://github.com/YongYuIT/Go-Stu/blob/master/hello_kafka/demo/test3_recv_from_kafka.go func GetMessageFromKafkaGroup(topicName string) )

	ctx, cancel := context.WithCancel(context.Background())
	//此方法爲阻塞方法,在重平衡(組員發生變化)時會退出。需要優化,避免重平衡後直接停止了消費。
	err = group.Consume(ctx, []string{topicName}, myHandler{p_name, topicName})

group.Consume是一個阻塞方法,一般來說需要開啓一個協程去執行(這裏只是Demo,所以沒有開啓協程)。函數要求傳入一個context對象,而這個context對象其實是由當前協程(主協程)派生出來的子context。向子協程傳入子context對象就相當將子協程的控制權交給了父協程(主協程),這樣主協程就可以隨時退出組消費。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章