天天看点

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,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

WithTimeout

WithDeadline

基本上一样,这个表示是超时自动取消,是多少时间后自动取消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调用会沿着链路通知到每一个子节点,因此实现了强并发控制,流程如图:

GoLang并发控制(下)

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

”,了解相关信息可以关注“

”。