九. Go并发编程--context.Context (5)

假设我们在 main 中并发调用了 f1 f2 两个函数,但是 f1 很快就返回了,但是 f2 还在阻塞

package main import ( "context" "fmt" "sync" "time" ) func f1(ctx context.Context) error { select { case <-ctx.Done(): return fmt.Errorf("f1: %w", ctx.Err()) case <-time.After(time.Millisecond): // 模拟短时间报错 return fmt.Errorf("f1 err in 1ms") } } func f2(ctx context.Context) error { select { case <-ctx.Done(): return fmt.Errorf("f2: %w", ctx.Err()) case <-time.After(time.Hour): // 模拟一个耗时操作 return nil } } func main() { // 超时 context ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() // f1 由于时间很短,会返回error,从而调用 cancel if err := f1(ctx); err != nil { fmt.Println(err) // ctx 调用cancel后,会传递到f1,f2 cancel() } }() go func() { defer wg.Done() if err := f2(ctx); err != nil { fmt.Println(err) cancel() } }() wg.Wait() fmt.Println("exit...") }

执行结果,可以看到 f1 返回之后 f2 立即就返回了,并且报错 context 被取消

root@failymao:/mnt/d/gopath/src/Go_base/daily_test# go run context/err_cancel.go f1 err in 1ms f2: context canceled exit...

这个例子其实就是 errgroup 的逻辑,是的它就是类似 errgroup 的简单逻辑

3.5 传递共享数据

一般会用来传递 tracing id, request id 这种数据,不要用来传递可选参数,这里借用一下饶大的一个例子,在实际的生产案例中我们代码也是这样大同小异

const requestIDKey int = 0 func WithRequestID(next http.Handler) http.Handler { return http.HandlerFunc( func(rw http.ResponseWriter, req *http.Request) { // 从 header 中提取 request-id reqID := req.Header.Get("X-Request-ID") // 创建 valueCtx。使用自定义的类型,不容易冲突 ctx := context.WithValue( req.Context(), requestIDKey, reqID) // 创建新的请求 req = req.WithContext(ctx) // 调用 HTTP 处理函数 next.ServeHTTP(rw, req) } ) } // 获取 request-id func GetRequestID(ctx context.Context) string { ctx.Value(requestIDKey).(string) } func Handle(rw http.ResponseWriter, req *http.Request) { // 拿到 reqId,后面可以记录日志等等 reqID := GetRequestID(req.Context()) ... } func main() { handler := WithRequestID(http.HandlerFunc(Handle)) http.ListenAndServe("http://www.likecs.com/", handler) } 3.6 防止 goroutine 泄漏

看一下官方文档的这个例子, 这里面 gen 这个函数中如果不使用 context done 来控制的话就会导致 goroutine 泄漏,因为这里面的 for 是一个死循环,没有 ctx 就没有相关的退出机制

func main() { // gen generates integers in a separate goroutine and // sends them to the returned channel. // The callers of gen need to cancel the context once // they are done consuming generated integers not to leak // the internal goroutine started by gen. gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } 四. 总结

对 server 应用而言,传入的请求应该创建一个 context,接受
通过 WithCancel , WithDeadline , WithTimeout 创建的 Context 会同时返回一个 cancel 方法,这个方法必须要被执行,不然会导致 context 泄漏,这个可以通过执行 go vet 命令进行检查

应该将 context.Context 作为函数的第一个参数进行传递,参数命名一般为 ctx 不应该将 Context 作为字段放在结构体中。

不要给 context 传递 nil,如果你不知道应该传什么的时候就传递 context.TODO()

不要将函数的可选参数放在 context 当中,context 中一般只放一些全局通用的 metadata 数据,例如 tracing id 等等

context 是并发安全的可以在多个 goroutine 中并发调用

4.1 使用场景

超时控制

错误取消

跨 goroutine 数据同步

防止 goroutine 泄漏

4.2 缺点

最显著的一个就是 context 引入需要修改函数签名,并且会病毒的式的扩散到每个函数上面,不过这个见仁见智,我看着其实还好

某些情况下虽然是可以做到超时返回提高用户体验,但是实际上是不会退出相关 goroutine 的,这时候可能会导致 goroutine 的泄漏,针对这个我们来看一个例子

我们使用标准库的 timeout handler 来实现超时控制,底层是通过 context 来实现的。我们设置了超时时间为 1ms 并且在 handler 中模拟阻塞 1000s 不断的请求,然后看 pprof 的 goroutine 数据

package main import ( "net/http" _ "net/http/pprof" "time" ) func main() { mux := http.NewServeMux() mux.HandleFunc("http://www.likecs.com/", func(rw http.ResponseWriter, r *http.Request) { // 这里阻塞住,goroutine 不会释放的 time.Sleep(1000 * time.Second) rw.Write([]byte("hello")) }) handler := http.TimeoutHandler(mux, time.Millisecond, "xxx") go func() { if err := http.ListenAndServe("0.0.0.0:8066", nil); err != nil { panic(err) } }() http.ListenAndServe(":8080", handler) }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgzjys.html