Skip to main content

Context

Context你必须掌握的多线程并发控制神器

下面是个机器狗监控的代码

package main

import (
"fmt"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup
wg.Add(1)

go func() {
defer wg.Done()
watchDog("【监控狗1】")
}()
wg.Wait()
}

//让协程提前退出
func watchDog(name string) {
//开启 for select循环,一直后台监控
for {
select {
default:
fmt.Println(name,"正在监控...")
}
time.Sleep(1*time.Second)
}
}

运行之后可以看到在不停的输出

协程如何退出?

但是修改全局变量的方法需要加锁以保证并发的安全性

改造代码,可以看到channel的方式非常优雅

package main

import (
"fmt"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup
wg.Add(1)

stopChan:=make(chan bool) //用来停止监控狗

go func() {
defer wg.Done()
watchDog(stopChan,"【监控狗1】")
}()

time.Sleep(time.Second*3)//先让监控狗监控3s
stopChan<- true
wg.Wait()
}

//让协程提前退出
func watchDog(stopChan chan bool, name string) {
//开启 for select循环,一直后台监控
for {
select {
case <- stopChan:
fmt.Println(name,"停止指令已收到,马上停止!")
return
default:
fmt.Println(name,"正在监控...")
}
time.Sleep(1*time.Second)
}
}

channel的局限性

用context.Context来改造代码

package main

import (
"fmt"
"golang.org/x/net/context"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup
wg.Add(1)
//生成一个可以空的/可以取消的context 参数context.Background()一般作为context树的根节点
ctx, stop := context.WithCancel(context.Background())

go func() {
defer wg.Done()
watchDog(ctx,"【监控狗1】")
}()

time.Sleep(time.Second*3)//先让监控狗监控3s
stop() //发停止指令
wg.Wait()
}

//让协程提前退出
func watchDog(ctx context.Context, name string) {
//开启 for select循环,一直后台监控
for {
select {
case <- ctx.Done():
fmt.Println(name,"停止指令已收到,马上停止!")
return
default:
fmt.Println(name,"正在监控...")
}
time.Sleep(1*time.Second)
}
}

可以看到和channel的效果是一样的

sync.WaitGroup原理就是计数器不为0时阻塞 channel原生就可以阻塞,所以我们完全可以用channel替代sync.WaitGroup

什么是Context

一个任务会有很多协程协作完成

一次http请求也会触发很多个协程的启动

而这些协程的启动还会启动更多的子协程 并且无法预知会有多少层协程 每一层又有多少协程

如果因为某些原因导致任务终止了 比如http请求取消了 那么它们启用的协程该怎么办呢? 该如何取消呢?

因为取消协程可以节约内存,提高性能,同时可以预防未知的bug,这就是它的好处

Context其实就是用来简化和解决这些问题的,而且它是并发安全的

一旦下达了取消的指令,那么被Context跟踪的那些协程都会收到取消的信号 就可以做清理和退出的操作

// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)

// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}

// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error

// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}
type Context interface {
Deadline() (deadline time.Time, ok bool) //获取设置的截止时间
Done() <-chan struct{} //返回一个只读的channel 类型为struct{}
Err() error //返回取消的错误原因
Value(key interface{}) interface{}//获取该context上绑定的值,是一个键值对
}

Context树

我们不需要去实现Context接口,因为go语言给我们提供了函数,我们可以根据需求生成不同的Context,通过这些函数可以生成一颗Context树

父Context发出取消信号的时候,子Context也会取消,这样就可以控制不同层级协程的退出

从功能上讲,有4种实现好了的Context

API

Context练习-秦皇找长生药

此示例应用于多个地方找一个:如多个文件夹中(pub des 未开始 进行中 已结束)找到某个单号

多人取出一人:百米赛跑 多人执行任务 一旦有人完成 所有人都停止做任务的场景

package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
ctx, stop := context.WithCancel(context.Background())

//秦始皇派了20支队伍去找长生药
//只要有一支队伍找到,则任务达成,全队返回咸阳
mych := make(chan bool)
for i := 0; i < 20; i++ {
go func(n int) {
fmt.Printf("第%d支队伍出发了\n", i)
mych <- true
x := i //把i存到临时变量中 很重要 因为访问同一块内存 最后i一定等于20
for {
flag := found()
if flag {
fmt.Printf("第%d队找到了长生药了\n", x)
stop()
}
}
}(i)
<-mych
}

<-ctx.Done()
fmt.Println("所有队伍返回咸阳")
}

func found() bool {
time.Sleep(time.Second)
//用随机数来模拟结果
rand.Seed(time.Now().Unix())
n := rand.Intn(10)
if n == 5 {
return true
} else {
return false
}
}

我们再来测试一下:

package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
sendTask := make(chan bool)
ctx, stop := context.WithCancel(context.Background())

//秦始皇派了20支队伍去找长生药
//只要有一支队伍找到,则任务达成,全队返回咸阳
for i := 0; i < 5; i++ {
go func(n int) {
x := i //把i存到临时变量中 很重要 因为访问同一块内存 最后i一定等于20
sendTask <- true //这句话一定要放在task任务之前,否则会因为任务长而长时间阻塞程序
fmt.Printf("第%d支队伍出发了\n", x)
for {
flag := found()
if flag {
fmt.Printf("第%d队找到了长生药了\n", x)
stop()
break
}
}

}(i)
<-sendTask
}

<-ctx.Done()
fmt.Println("停止指令已收到,马上停止!")
fmt.Println("所有队伍返回咸阳")
}

func found() bool {
time.Sleep(time.Second)
//用随机数来模拟结果
rand.Seed(time.Now().Unix())
n := rand.Intn(20)
if n == 11 {
return true
} else {
return false
}
}

如何通过Context实现日志跟踪

思路:必须有一个唯一的ID来标识这次请求;调用了哪些函数,执行了哪些代码;然后通过这个ID吧日志信息串联起来,这样就形成了日志的轨迹

上述实现的核心就是Context的传值功能