Go语言协程
什么是Goroutine
在Go语言中goroutine一般翻译为Go协程或Go程;在《Effective Go》中对其有这样一段介绍:
百度翻译:它们之所以被称为goroutines,是因为现有的术语线程、协程、进程等等传达了不准确的含义。goroutine有一个简单的模型:它是一个与同一地址空间中的其他goroutine同时执行的函数。它重量轻,只需分配堆栈空间即可。堆栈一开始很小,所以很便宜,并通过根据需要分配(和释放)堆存储来增长。
可以得知goroutine的特殊就是轻量,低成本,更易用。先看一下它是怎么使用的。
启动一个Goroutine
在调用函数或者方法前面加上关键字** go**,就可以运行一个新的 goroutine,调用后会立即返回。每一个goroutine之间是并发执行的,执行顺序并是不确定的。我们来看下面例子:
func echo(tag string) {
fmt.Println("[" + tag + "] goroutine running")
}
func main() {
go echo("G1")
fmt.Println("main stop")
}
如果你直接运行这段代码,大多数情况下应该是看不到echo函数执行的,原因是 Go 协程调用后会立即返回,执行下一行。如果 Go 主协程终止,则程序终止,其他 Go 协程也不会继续运行。如果要正常看到echo的输出可以在Go主协程结束前进行等待,比如sleep几秒:
time.Sleep(5 * time.Second)
显然这样不够优雅,因为实际情况中我们并不能确定go协程执行多久,也就没法确定sleep等待的时间。下面我们介绍一直更合适的方法sync.WaitGroup。
使用 sync.WaitGroup
WaitGroup用于等待一组线程的结束;父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。有三个到处方法分别是:Add、Done、Wait(); 下面我们重写一下上面的代码:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("[Go 1]: this is Goroutine 1")
}()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("[Go 2]: this is Goroutine 1")
}()
wg.Wait() // 此处会阻塞等待计数器归零
WaitGroup使用时需要注意: 1、零值可以,无需单独初始化; 2、Add数和Doen属需要一一对应, 3、Done 要在进程最后执行;Done 相当于 Add(-1);如果计数器小于0会panic
goroutine 使用起来固然方便,但是做到控制好goroutine的生命周期却不容易,如果只是简单的用go 关键字启动后不管,无法确定或控制其停止可能会产生大量野生goroutine,特殊情况下可能会造成内存泄漏等问题。下面我们介绍两种常见的goroutine控制方法:
使用 channel
我们需要重写一下echo函数,让它接受一个chan参数,用来接受来自主程的关闭信号;代码如下
func echo_chan(tag string, stop chan struct{}) {
begin := true
go func() {
select {
case <-stop:
fmt.Println(tag + "stop!")
begin = false
}
}()
for begin {
// 模拟go程进行某些操作
fmt.Println("[" + tag + "]" + time.Now().String())
time.Sleep(time.Second)
}
}
由本例只需要接受信号,不需要传递具体消息内容 chan struct{} 类型的channel以减少开销,方法中我们重新创建了一个goroutine用来监听stop 的信号;接受到信号后通过修改begin为false 来停止任务;通常情况下可能是调用任务对象的stop方法比如http server 中的Shutdown()。
使用 context
context 与channel 类似; 通过 鉴定 ctx.Done()控制goroutine。具体代码如下:
func echo_ctx(tag string, ctx context.Context) {
begin := true
go func() {
select {
case <-ctx.Done():
fmt.Println(tag + "stop!")
begin = false
}
}()
for begin {
fmt.Println("[" + tag + "]" + time.Now().String())
time.Sleep(time.Second)
}
}