golang 协程中使用 context 细节

后端 / 2023-03-04

前言

为了让我们服务承载更大的并发量,通常我们使用线程来优化自己的代码。
线程是cpu的最小调度单位 频繁创建线程 显然是 很大的开销。 于是乎协程诞生了,轻量级线程 可随时切换。 在go 中 协程的 开销非常小 2kb 相比于 java 的 2mb 要小很多。

GMP调度模型示例
image.png

如何开一个携程呢?

go fnName()

我们只需要 使用 go 关键字 就可以轻松开辟一个协程。 大家都知道 控制线程可不是一件容易的事情,于是 go 引出了 context 上下文。

当我们 需要 设置这个 协程 10秒 超时结束。 那么我们可以这样写

示例代码

const maxTimeout = time.Second * 10

func (p P2MDispatcher) Dispatch(ctx context.Context, e *larkim.P2MessageReceiveV1) {
	// 只处理有的事件
	var req *larkim.ReplyMessageReq
	var h handler.Handler[larkim.P2MessageReceiveV1]
	var err error
	ctx, cancelFn := context.WithTimeout(ctx, maxTimeout)
	defer cancelFn()
	go func() {
		client := utils.NewLarkClientUtil().Client()
		if h, err = filter(e, p.handlers); err != nil {
			return
		}
		if req = h.Process(ctx, e); req == nil {
			return
		}
		if _, err := client.Im.Message.Reply(ctx, req); err != nil {
			logrus.Error(err)
			return
		}
	}()
}

坑来了

这段代码乍一看没什么问题,但是实际上有很大的隐患。

通过 context.WithTimeout 我们创建一个 超时上下文。

当我们将上述服务运行

ERRO[0013] 获取消息体失败:Get "https://open.feishu.cn/open-apis/im/v1/messages/om_c71055ca3189f909f64371ec4d8bc2f6": context canceled 

可以看到 被超时取消了。

这是什么原因呢?

将 ctx 放在协程里面是为了可以在主程序中同时处理多个操作,而不会因为其中一个操作超时而阻塞整个程序。

当一个操作需要使用超时上下文时,我们通常会在一个单独的 goroutine 中执行该操作,并将超时上下文传递给这个 goroutine。这样,即使该操作超时,也不会阻塞主程序中的其他操作。

所以需要我们将 ctx 放到 协程中去