golang中的协程上下文

在使用grpc的时候,对通信协议有着调用链追踪的需求

这部分要封装进框架,对使用方透明,因此协程上下文(Goroutine Local Storage)的用法顺理成章

但是搜索了些资料,发现golang并没有gls

google提供的解决方法是使用golang.org/x/net/context包来传递上下文信息

google的设计理念:context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}

// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error

// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)

// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

从这个接口设计可以看出,Context主要是设计传值(Value接口),和超时或失败取消机制的(Done和Deadline接口)

超时和失败取消机制

这部分很简单,写个小demo就熟悉了

首先明确使用场景

  1. 对某个未知操作时间的函数(一般是跨网络),做超时机制

  2. 同时进行多个协程任务时,某个任务失败,需要取消其他任务的执行

先写一个work函数,来模拟未知操作时间的情况

1
2
3
4
5
6
7
8
9
10
11
func work(stop *bool) error {                                                      
r := rand.New(rand.NewSource(int64(time.Now().Second())))
for !*stop {
if r.Int() % 10 >= 9 {
break
}
time.Sleep(100 * time.Millisecond)
}
fmt.Println("work done")
return nil
}

这个函数在平均概率上的操作时间是1秒,但是可能超过1秒或者不到1秒完成

再写一个work的辅助函数,用context来管理work的取消或者超时

1
2
3
4
5
6
7
8
9
10
11
12
13
func doWork(ctx context.Context) error {                                           
c := make(chan error,1)
stop := false
go func() {c <- work(&stop)}()
select {
case <- ctx.Done():
stop = true
<-c //wait for work
return ctx.Err()
case err := <-c:
return err
}
}

这个函数利用管道同时对ctx.Done()和work函数进行监控,当执行ctx.Done()时,会取消work函数的执行

这两个完成,就能模拟上述两种情况了

1
2
3
4
5
6
timeout := 1 * time.Second                                                  
ctx, _ := context.WithTimeout(context.Background(), timeout)
err := doWork(ctx)
if err != nil {
fmt.Println(err)
}

当时间超过1秒后,ctx.Done()返回,打印出超时信息

1
2
work done
context deadline exceeded
1
2
3
4
5
6
7
8
9
10
ctx, cancel := context.WithCancel(context.Background())                     
go func() {
nouse := false
work(&nouse)
cancel()
}()
err = doWork(ctx)
if err != nil {
fmt.Println(err)
}

这里并发同时运行了两个协程

有一个协程完成了,这里把它看作是出错了,因此对其他协程进行了cancel

1
2
3
work done //协程A出错
work done //执行ctx.Done()后对work B进行取消
context canceled

传值

前面是用withTimeout和withCancel来继承了ctx

下面用传值来继承ctx

1
func WithValue(parent Context, key interface{}, val interface{}) Context

利用这种方式,在context携带需要的信息,然后用ctx.Value().(type)的方式将值取出

这种方式是协程安全的,因为只有一次写入WithValue,其他都只能进行读取,不会修改父ctx的value值

为什么google要独树一帜做这样的事情呢,我认为按照golang的设计理念,可能是希望去除含义不明的全局状态代码,显式的在函数加入ctx参数,让代码一目了然

gls野路子

golang实际还是有gls的第三方库的

https://github.com/tylerb/gls

https://github.com/jtolds/gls

第二个库没看明白

第一个库是通过runtime包输出stack信息来获取gid,然后根据gid做一个map[uint64]Values和锁的结构,get,set并且手动的释放gls资源

需要注意的是,当你的函数需要协程执行时,需要用gls.Go来继承父协程的gls资源

1
func Go(f func())

调用这个创建协程时,子协程的gls资源,在退出时会自动清除

由于这个Go的参数f func()没有参数,因此需要使用闭包的技巧来使用他

利用闭包可以将某个变量和函数作为整个对象的方式创建一个包含值的函数对象

1
2
3
4
5
6
7
8
9
10
11
f := func(x int) func (){                                                     
return func() {
fmt.Println(x)
}
}

f1 := f(0)
f2 := f(1)

gls.Go(f1)
gls.Go(f2)

总结

gls这个野路子在做demo的时候还是挺好用的