Goroutine
是Go语言最重要的机制,Goroutine
将复杂的需要异步的IO调用抽象成同步调用,符合人类正常的顺序思维,极大的简化了IO编程的难度。如同线程一样,对Goroutine
既要掌握基本的用法,更要很好的控制Goroutine
的退出机制。本文介绍一种Goroutine
的退出思路。
通常Goroutine
会因为两种情况阻塞:
IO操作,比如对
Socket
的Read
。channel
操作。对一个chan的读写都有可能阻塞Goroutine
。
对于情况1,只需要关闭对应的描述符,阻塞的Goroutine
自然会被唤醒。
重点讨论情况2。并发编程,Goroutine
提供一种channel
机制,channel
类似管道,写入者向里面写入数据,读取者从中读取数据。如果channel
里面没有数据,读取者将阻塞,直到有数据;如果channel
里面数据满了,写入者将因为无法继续写入数据而阻塞。
如果在整个应用程序的生命周期里,writer和reader都表现为一个Goroutine
,始终都在工作,那么如何在应用程序结束前,通知它们终止呢?在Go中,并不推荐像abort线程那样,强行的终止Goroutine
。因此,抽象的说,必然需要保留一个入口,能够跟writer或reader通信,以告知它们终止。
我们先看reader。我们首先可以想到,利用close
函数关闭正在读取的channel
,从而可以唤醒reader,并退出。但是考虑到close
并不能很好的处理writer(因为writer试图写入一个已经close的channel,将引发异常)。因此,我们需要设计一个额外的只读channel
用于通知:
type routineSignal struct {
done <-chan struct{}
}
routineSignal
的实例,应当通过外部生成并传递给reader,例如:
func (r *reader)init(s *routineSignal) {
r.signal = s
}
在reader的循环中,就可以这么写:
func (r *reader)loop() {
for {
select {
case <-r.signal.done:
return
case <-r.queue:
....
}
}
}
当需要终止Goroutine
的时候只需要关闭这个额外的channel
:
close(signal.done)
看起来很完备了,这可以处理大部分的情况了。这样做有个弊端,尽管,我们可以期望close
唤醒Goroutine
进而退出,但是并不能知道Goroutine
什么时候完成退出,因为Goroutine
可能在退出前还有一些善后工作,这个时候我们需要sync.WaitGroup
。改造一下routineSignal
:
type routineSignal struct {
done chan struct{}
wg sync.WaitGroup
}
增加一个sync.WaitGroup的实例,在Goroutine
开始工作时,对wg加1,在Goroutine
退出前,对wg减1:
func (r *reader)loop() {
r.signal.wg.Add(1)
defer r.signal.wg.Done()
for {
select {
case <-r.signal.done:
return
case <-r.queue:
....
}
}
}
外部,只需要等待WaitGroup
返回即可:
close(signal.done)
signal.wg.Wait()
只要Wait()
返回就能断定Goroutine
结束了。
推导一下,不难发现,对于writer也可以采用这种方法。于是,总结一下,我们创建了一个叫routineSignal
的结构,结构里面包含一个chan
用来通知Goroutine
结束,包含一个WaitGroup
用于Goroutine
通知外部完成善后。这样,通过这个结构的实例优雅的终止Goroutine
,而且还可以确保Goroutine
终止成功。
有疑问加站长微信联系(非本文作者)