Go语言协程


什么是Goroutine

在Go语言中goroutine一般翻译为Go协程或Go程;在《Effective Go》中对其有这样一段介绍: image.png

百度翻译:它们之所以被称为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 +