參考: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對象就相當將子協程的控制權交給了父協程(主協程),這樣主協程就可以隨時退出組消費。