之前对context的了解比较浅薄,只知道它是用来传递上下文信息的对象;
对于Context本身的存储、类型认识比较少。
最近又正好在业务代码中发现一种用法:在每个协程中都会复制一份新的局部context对象,想探究下这种写法在性能上有没有弊端。
jobList := []func() error{
s.task1,
s.task2,
s.task3,
s.task4,
}
if err := gconc.GConcurrency(jobList); err != nil {
resource.LoggerService.Error(ctx, "exec concurrency job list error", logit.Error("error", err))
}
func (s *Service) task1() (err error) {
if !s.isLogin() {
return nil
}
// 新局部变量,值来自全局的context对象
ctx := s.ctx
return nil
}
golang.org/x/net/context,是golang中的一个标准库,主要作用就是创建一个上下文,实现对程序中创建的协程通过传递上下文信息来实现对协程的管理。
在Go语言中,可以通过多种方式创建Context:
Background()和TODO():这两个函数分别用于创建空的Context,通常作为根节点使用
WithCancel(parent Context):创建一个可取消的Context,并返回一个取消函数
WithDeadline(parent Context, deadline time.Time):创建一个带有截止时间的Context,并返回一个取消函数
WithTimeout(parent Context, timeout time.Duration):创建一个带有超时控制的Context,它等同于WithDeadline(parent, time.Now().Add(timeout))。
WithValue(parent Context, key, val interface{}):创建一个带有键值对的Context,同时保留父级Context的所有数据。
上面介绍了几种创建Context对象的方法,包括创建可取消的Context、带有截止时间的Context以及带有键值对的Context
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Context接口定义了四个方法:
通过分析Context接口,可以知道Context对象都是对Context接口的实现,如空Context对象就是emptyCtx,它不包含任何值
type emptyCtx struct{}
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (emptyCtx) Done() <-chan struct{} {
return nil
}
func (emptyCtx) Err() error {
return nil
}
func (emptyCtx) Value(key any) any {
return nil
}
而带有超时控制的Context其实就是一个带有定时器并且实现了Context接口的对象
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
对于存在若干个协程的程序,协程之前可能会存在如下的关系,这就需要在父协程关闭时,对子协程及时关闭;
否则协程可能会持续存在与内存中,造成内存泄漏。
Context对子协程的控制销毁就是基于协程创建的过程中,为每个子协程创建子context,以WithCancel()方法为例进行分析:
WithCancel()会返回一个新的子context和一个上下文取消方法,当执行cancel时,当前协程下的子context都会被销毁。
package main
import (
"context"
"fmt"
"time"
)
// worker 是一个模拟工作的函数,它接受一个 context 并根据 context 的状态来决定是否继续工作。
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
// 当 context 被取消时,worker 会收到通知并退出循环。
fmt.Printf("Worker %d: stopping\n", id)
return
default:
// 继续执行模拟的工作。
fmt.Printf("Worker %d: working\n", id)
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 创建一个带取消功能的 context。
ctx, cancel := context.WithCancel(context.Background())
// 启动多个 worker 协程。
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// 让主协程等待一段时间,然后取消 context。
time.Sleep(5 * time.Second)
fmt.Println("Main: canceling context")
cancel()
// 等待一段时间以确保所有 worker 都有机会响应取消信号。
// 在实际应用中,你可能需要一个更复杂的机制来等待所有 worker 退出。
time.Sleep(2 * time.Second)
fmt.Println("Main: exiting")
}
通常使用带键值对的Contex对象,即ValueContext传递信息。
package main
import (
"context"
"fmt"
"time"
)
// requestIDKey 是一个用于在 context 中存储请求 ID 的键。
type requestIDKey struct{}
// worker 是一个模拟工作的函数,它接受一个 context 并从中提取请求 ID。
func worker(ctx context.Context, taskName string) {
// 从 context 中获取请求 ID。
requestID := ctx.Value(requestIDKey{}).(string)
fmt.Printf("%s: started, request ID: %s\n", taskName, requestID)
// 模拟工作。
time.Sleep(2 * time.Second)
// 完成工作。
fmt.Printf("%s: completed, request ID: %s\n", taskName, requestID)
}
func main() {
// 创建一个带有请求 ID 的 context。
requestID := "12345"
ctx := context.WithValue(context.Background(), requestIDKey{}, requestID)
// 启动多个 worker 协程。
tasks := []string{"Task A", "Task B", "Task C"}
for _, task := range tasks {
go worker(ctx, task)
}
// 等待一段时间以确保所有 worker 都有机会完成工作。
// 在实际应用中,你可能需要一个更复杂的机制来等待所有 worker 退出。
time.Sleep(6 * time.Second)
fmt.Println("Main: all tasks completed or timed out")
}
写一个示例代码,通过debug来分析
type UserInfo struct {
UID int
Name string
Address *Address
}
type Address struct {
X int
Y int
}
func TestValueContext(t *testing.T) {
ctx := context.Background()
address := &Address{
X: 101,
Y: 202,
}
withValue := context.WithValue(ctx, UserInfo{}, UserInfo{
UID: 1,
Name: "test",
Address: address,
})
// 新变量 拷贝的context对象
copyCtx := withValue
fmt.Println(copyCtx)
}
通过debug可以看到,和普通的变量赋值一样,拷贝出的copyCtx对象就是ctx对象的值;
拷贝的过程是浅拷贝,当ctx中包含指针时,拷贝的是其地址。
通过上面的分析我们可以知道以下几点事实
因此,问题代码中对ctx的拷贝,不考虑代码清晰度的情况下,并没有额外的意义,而且在被拷贝的Context对象很大时,会有额外的内存开销。
func (s *Service) task1() (err error) {
if !s.isLogin() {
return nil
}
// 无意义的局部变量,值来自全局的context对象
ctx := s.ctx
return nil
}
Go语言高并发系列三:context - 掘金