协程goroutine是Go语言实现并发的“核心武器”,上一节我们讲解高并发原理时知道,goroutine类似于线程的概念,但是goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。程序员在编写并发程序是并不需要自己去写“调度”相关的部分,如果需要让任务并发执行,你只需要把这个任务包装成一个函数,然后开启一个goroutine去让他执行就好了,就是这么简单。
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。那么其他的goroutine要怎么启动呢?接着往下看!
一、goroutine的使用
Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}
但你会发现,上面程序运行后只输出了一个"main goroutine done!",Why?因为还没等go出来的goroutine执行或者执行完,main goroutine就执行完结束了!只要main goroutine结束,其中创建的所有子goroutine都会随之消亡,无论执行到什么节点。所以,怎么能够让子goroutine执行完在结束呢?
最简单粗暴的方式是time.Sleep。
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。因为创建一个goroutine也需要时间开销,虽然很小。
启动多个goroutine
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
这里使用了sync.WaitGroup来实现goroutine的同步,你会发现每次执行打印数字的顺序不一样,因为10个goroutine是并发执行的,而goroutine的调度是随机的。
二、goroutine和线程
二者的区别在 高并发原理 一节有介绍,这里简单说一下。
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
三、goroutine调度
G-P-M调度模型在 高并发原理 一节有介绍,这里不做赘述。
四、goroutine泄露
goroutine如果使用不规范,容易造成泄露,进而影响程序稳定性。相关知识下一次总结。