GoLang并发控制(下)

简介:

context的字面意思是上下文,是一个比较抽象的词,字面上理解就是上下层的传递,上会把内容传递给下,在go中程序单位一般为goroutine,这里的上下文便是在goroutine之间进行传递。

根据现实例子来讲,最常看到context的便是web端。一个网络请求request请求服务端,每一个request都会开启一个goroutine,这个goroutine在逻辑处理中可能会去开启其他的goroutine,例如去开启一个MongoDB的连接,一个request的goroutine开启了很多个goroutine时候,需要对这些goroutine进行控制,这时候就需要context来进行对这些goroutine进行跟踪。即一个请求Request,会需要多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。

例子讲述完毕,用go的风格再讲一次。

在每一个goroutine在执行之前,都要知道程序当前的执行状态,这些状态都被封装在context变量中,要传递给要执行的goroutine中去,这个上下文就成为了传递与请求同生存周期变量的标准方法。

注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特别快,每一个版本都变得越来越好,自己第一次接触go语言的时候才1.9版本,实习公司用的好像是1.7,研发团队解体后现在实习用的版本是1.11 短时间版本就如此之大,1.10版本G-M模型改为G-P-M模型,听闻1.12社区会再次优化GC垃圾回收,引入分代)

Context接口

Context的接口定义的比较简洁,我们看下这个接口的方法。


1type Context interface {
2 Deadline() (deadline time.Time, ok bool)
3
4 Done() <-chan struct{}
5
6 Err() error
7
8 Value(key interface{}) interface{}
9}

这个接口共有4个方法,了解这些方法的意思非常重要,这样我们才可以更好的使用他们。

Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。

Err方法返回取消的错误原因,因为什么Context被取消。

Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。

Context的继承衍生


1func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
2func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
3func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
4func WithValue(parent Context, key, val interface{}) Context
5

这四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。 WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

WithTimeoutWithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到

引用飞雪无情的代码:


1func main() {
2 ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for {
3 select { case <-ctx.Done():
4 fmt.Println("监控退出,停止了...") return
5 default:
6 fmt.Println("goroutine监控中...")
7 time.Sleep(2 * time.Second)
8 }
9 }
10 }(ctx)
11
12 time.Sleep(10 * time.Second)
13 fmt.Println("可以了,通知监控停止")
14 cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了
15 time.Sleep(5 * time.Second)
16
17}

context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。
在goroutine中,使用select调用<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。
那么是如何发送结束指令的呢?这就是示例中的cancel函数啦,它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。

在引用一段多控制


1func main() {
2 ctx, cancel := context.WithCancel(context.Background())
3 go watch(ctx,"【监控1】")
4 go watch(ctx,"【监控2】")
5 go watch(ctx,"【监控3】")
6
7 time.Sleep(10 * time.Second)
8 fmt.Println("可以了,通知监控停止")
9 cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了
10 time.Sleep(5 * time.Second)
11}
12
13func watch(ctx context.Context, name string) { for {
14 select { case <-ctx.Done():
15 fmt.Println(name,"监控退出,停止了...") return
16 default:
17 fmt.Println(name,"goroutine监控中...")
18 time.Sleep(2 * time.Second)
19 }
20 }
21}

示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

在引用一次潘少大佬的代码:


1package mainimport ( "context"
2 "crypto/md5"
3 "fmt"
4 "io/ioutil"
5 "net/http"
6 "sync"
7 "time")type favContextKey string
8
9func main() {
10 wg := &sync.WaitGroup{}
11 values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
12 ctx, cancel := context.WithCancel(context.Background()) for _, url := range values {
13 wg.Add(1)
14 subCtx := context.WithValue(ctx, favContextKey("url"), url) go reqURL(subCtx, wg)
15 }
16
17 go func() {
18 time.Sleep(time.Second * 3)
19 cancel()
20 }()
21
22 wg.Wait()
23 fmt.Println("exit main goroutine")
24}func reqURL(ctx context.Context, wg *sync.WaitGroup) {
25 defer wg.Done()
26 url, _ := ctx.Value(favContextKey("url")).(string) for {
27 select { case <-ctx.Done():
28 fmt.Printf("stop getting url:%s\n", url) return
29 default:
30 r, err := http.Get(url) if r.StatusCode == http.StatusOK && err == nil {
31 body, _ := ioutil.ReadAll(r.Body)
32 subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
33 wg.Add(1) go showResp(subCtx, wg)
34 }
35 r.Body.Close()
36 //启动子goroutine是为了不阻塞当前goroutine,这里在实际场景中可以去执行其他逻辑,这里为了方便直接sleep一秒
37 // doSometing()
38 time.Sleep(time.Second * 1)
39 }
40 }
41}
42
43func showResp(ctx context.Context, wg *sync.WaitGroup) {
44 defer wg.Done() for {
45 select { case <-ctx.Done():
46 fmt.Println("stop showing resp") return
47 default: //子goroutine里一般会处理一些IO任务,如读写数据库或者rpc调用,这里为了方便直接把数据打印
48 fmt.Println("printing ", ctx.Value(favContextKey("resp")))
49 time.Sleep(time.Second * 1)
50 }
51 }
52}

首先调用context.Background()生成根节点,然后调用withCancel方法,传入根节点,得到新的子Context以及根节点的cancel方法(通知所有子节点结束运行),这里要注意:该方法也返回了一个Context,这是一个新的子节点,与初始传入的根节点不是同一个实例了,但是每一个子节点里会保存从最初的根节点到本节点的链路信息 ,才能实现链式。

程序的reqURL方法接收一个url,然后通过http请求该url获得response,然后在当前goroutine里再启动一个子groutine把response打印出来,然后从ReqURL开始Context树往下衍生叶子节点(每一个链式调用新产生的ctx),中间每个ctx都可以通过WithValue方式传值(实现通信),而每一个子goroutine都能通过Value方法从父goroutine取值,实现协程间的通信,每个子ctx可以调用Done方法检测是否有父节点调用cancel方法通知子节点退出运行,根节点的cancel调用会沿着链路通知到每一个子节点,因此实现了强并发控制,流程如图:

2fe9c5ccb94b471effe32c1e0d0b54e79874724b

context使用规范

最后,Context虽然是神器,但开发者使用也要遵循基本法,以下是一些Context使用的规范:

 ●  Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一个结构体当中,显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx;
 ●  Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO;
 ●  Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;

 ●  The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的;


原文发布时间为:2018-11-18

本文作者:不喜欢夜雨天

本文来自云栖社区合作伙伴“Golang语言社区”,了解相关信息可以关注“Golang语言社区”。

相关文章
|
29天前
|
安全 Go
Go语言并发新特性:单向通道的读写控制
Go语言并发新特性:单向通道的读写控制
20 0
|
29天前
|
数据采集 安全 Go
Go并发优化的9大技巧,效果立竿见影
Go并发优化的9大技巧,效果立竿见影
17 0
|
29天前
|
Go
Go 如何解决 并发中的竞争状态
Go 如何解决 并发中的竞争状态
17 0
|
29天前
|
安全 Go
Go语言并发:释放程序潜能的魔力
Go语言并发:释放程序潜能的魔力
12 0
|
1月前
|
Go
浅谈Golang并发控制WaitGroup
浅谈Golang并发控制WaitGroup
21 0
|
1月前
|
存储 监控 Cloud Native
云原生系列Go语言篇-并发 Part 2
既然已经讲解了Go为并发所提供的基础工具,我们就来学习一些并发的最佳实践和模式吧。
46 0
|
19天前
|
存储 Go 调度
开心档之 Go 并发
开心档之 Go 并发
|
24天前
|
Go
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
深度探讨 Golang 中并发发送 HTTP 请求的最佳技术
38 0
|
29天前
|
网络协议 搜索推荐 Go
Go开发不得不学的 并发 与 反射
Go开发不得不学的 并发 与 反射
15 0
|
29天前
|
安全 Go
Go语言并发打印:通过通道实现协同输出
Go语言并发打印:通过通道实现协同输出
11 0