前言
为了让我们服务承载更大的并发量,通常我们使用线程来优化自己的代码。
线程是cpu的最小调度单位 频繁创建线程 显然是 很大的开销。 于是乎协程诞生了,轻量级线程 可随时切换。 在go 中 协程的 开销非常小 2kb 相比于 java 的 2mb 要小很多。
GMP调度模型示例
如何开一个携程呢?
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 放到 协程中去