GoLang并发控制(下)

作者:不喜欢夜雨天 链接:https://www.jianshu.com/p/757bafad74ee 來源:简书

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调用会沿着链路通知到每一个子节点,因此实现了强并发控制,流程如图:

044svco84sif9rjebqagmar0fp.png

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中是安全的;

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2018-11-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java 成神之路

spring 之 import标签、alias标签、beans标签 解析

606100
来自专栏林冠宏的技术文章

浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因

作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:htt...

425100
来自专栏MelonTeam专栏

Viewpager循环滑动的实现

导语 本文讲述实现ViewPager循环滑动效果的两种方案: 方案1: 复写ViewPager或者Adapter,扩展dataList,左右各加1...

23760
来自专栏郭耀华‘s Blog

android中handler用法总结

一、Handler的定义:  Handler主要接收子线程发送的数据, 并用此数据配合主线程更新UI,用来跟UI主线程交互用。比如可以用handler发送...

41390
来自专栏飞雪无情的博客

Go语言实战笔记(二十)| Go Context

控制并发有两种经典的方式,一种是WaitGroup,另外一种就是Context,今天我就谈谈Context。

21430
来自专栏chenssy

【死磕 Spring】----- IOC 之解析 bean 标签:解析自定义标签

前面四篇文章都是分析 Bean 默认标签的解析过程,包括基本属性、六个子元素(meta、lookup-method、replaced-method、constr...

12640
来自专栏腾讯Bugly的专栏

深入浅出 Retrofit,这么牛逼的框架你们还不来看看?

Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient,再到对前面这些网络基础框架的封装,比如 ...

39060
来自专栏腾讯大讲堂的专栏

深入浅出 Retrofit,这么牛逼的框架你们还不来看看?

文章来源:腾讯Bugly Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient,再到对前面这些...

33150
来自专栏我和未来有约会

如何在silverlihgt中使用右键

一般我们在silverlight中点击右键会出现如下的对话筐. ? ? 在flash中 其提供了一个可定制话的右键菜单系统.(ContextMenu) 这个...

21570
来自专栏Android 开发学习

data-binding 踩坑记

22340

扫码关注云+社区

领取腾讯云代金券