在Go语言中,启动一个协程Goroutine很方便,一行代码的事儿,比如go runTask()
。但是如果想要在这个goroutine的外部,来关闭这个goroutine呢?
举个例子,在web请求A中,启动了一个goroutine来运行某个任务,就像这样 go runTask()
。它的内部是一个for循环,循环获取数据,同时检查任务的运行状态。如果不再是运行状态了,就可以退出了。
还有请求B,我们希望通过调用它来关闭请求A中启动的goroutine,要如何操作呢?通过channel发消息,那么这个channel要怎么在两个请求之间使用呢?使用全局的channel即可方便在请求中访问使用了。
这里会有一个问题,即每次请求A都将启动一个channel,在请求B中如何关闭一个特定的goroutine呢?答案自然是map,每个channel和运行的任务id映射起来即可。每次通过取出特定的channel,然后关闭它。
此外,在看过很多关于goroutine的优雅退出文章后,我们将采用context来退出,因为context多次关闭也不会panic,而普通的channel是不允许关闭两次否则panic。Go文档里的示例是这样的:
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:
}
}
}
要有一个context实例。然后先处理业务,之后在业务处理完成后,通过select来接受多个channel的输入,这里就接收了context的Done()事件。
或许眼尖的你已经发现了,这个ctx还传递到了DoSomething的函数里,为啥呢?因为context的关闭事件是一个广播操作,其实就是channel的关闭,会导致所有监听在此channel上的收到消息,那么通过传递ctx到下层的DoSomething函数里,在ctx关闭时候,DoSomething也会响应这个关闭动作。
接下里我们看看实际的操作吧。
// 存储context的对象
type CtxUtils struct {
Cancel context.CancelFunc
Ctx context.Context
}
// map
var TasksCtx = make(map[uint64]*utils.CtxUtils)
在web请求中,检测该任务是否已经有运行中的goroutine,有则删除并且取消。其中context.WithCancel()是一个可以取消运行的context。还有WithDeadline的context,它是可以设置超时时间的context。只要我们调用这个cancel函数,以下监听的ctx.Done()的都会收到消息,我们就可以退出了。
if prevTaskCtx, ok := TasksCtx[taskId]; ok {
delete(TasksCtx, taskId)
prevTaskCtx.Cancel()
}
ctx, cancel := context.WithCancel(context.Background())
TasksCtx[taskId] = &utils.CtxUtils{
Ctx: ctx,
Cancel: cancel,
}
go XxxMgr.runTaskWorker(taskId, ctx)
对应的runTaskWorker
里,我们将ctx传递过去了。
func (cm *XxManager) runTaskWorker(taskId uint64, ctx context.Context) {
defer func() {
utils.Sugar.Infof("task %d exit", taskId)
if _, ok := TasksCtx[taskId]; ok {
delete(TasksCtx, taskId)
}
}()
for {
select {
case <- ctx.Done():
utils.Sugar.Infof("Goroutine [Task: %d] Exit: run task worker", taskId)
return
default:
doSomethings(ctx)
// or
time.Sleep(10 * time.Second)
}
}
看过很多人写的channel优雅退出,奈何自己第一次在实际业务中写出来,还是很开心的。。。哈哈哈