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语言社区”。

相关文章
|
1月前
|
Go 调度 开发者
CSP模型与Goroutine调度的协同作用:构建高效并发的Go语言世界
【2月更文挑战第17天】在Go语言的并发编程中,CSP模型与Goroutine调度机制相互协同,共同构建了高效并发的运行环境。CSP模型通过通道(channel)实现了进程间的通信与同步,而Goroutine调度机制则确保了并发任务的合理调度与执行。本文将深入探讨CSP模型与Goroutine调度的协同作用,分析它们如何共同促进Go语言并发性能的提升。
|
2月前
|
存储 安全 Java
浅谈Go并发原语
浅谈Go并发原语
35 0
|
3月前
|
安全 Go
Go语言并发新特性:单向通道的读写控制
Go语言并发新特性:单向通道的读写控制
41 0
|
3月前
|
数据采集 安全 Go
Go并发优化的9大技巧,效果立竿见影
Go并发优化的9大技巧,效果立竿见影
56 0
|
3月前
|
Go
Go 如何解决 并发中的竞争状态
Go 如何解决 并发中的竞争状态
39 0
|
3月前
|
安全 Go
Go语言并发:释放程序潜能的魔力
Go语言并发:释放程序潜能的魔力
26 0
|
1月前
|
程序员 Go 数据处理
|
1月前
|
SQL Go 数据库
【Sentinel Go】新手指南、流量控制、熔断降级和并发隔离控制
【2月更文挑战第12天】随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。
56 0
|
1月前
|
监控 Java 编译器
Go语言内存与并发性能综合优化策略
【2月更文挑战第11天】Go语言以其高效的并发处理能力和简洁的内存管理机制成为了现代软件开发中的热门选择。然而,在实际应用中,如何综合优化Go程序的内存使用和并发性能,仍然是一个值得探讨的话题。本文将深入探讨Go语言内存与并发性能的综合优化策略,包括内存布局优化、并发模式设计、资源池化以及性能监控与分析等方面,旨在帮助开发者全面提升Go程序的整体性能。
|
1月前
|
Java Go 调度
Go语言并发性能提升实战
【2月更文挑战第11天】Go语言以其出色的并发处理能力在高性能应用中占据了一席之地。然而,要想充分利用Go的并发优势,实现性能的最大化,还需要掌握一些关键的优化技巧。本文将深入探讨Go语言并发性能提升的关键技术,包括goroutine调度、channel使用、sync原语选择等方面,帮助开发者更好地理解和优化Go程序的并发性能。