💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] goroutine 是 Golang 语言中的轻量级线程实现,由 Golang 运行时(Runtime)管理。 Goroutine 是 Go 语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。 Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine -> main goroutine,当 Go 程序启动时它会自动创建。 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能——goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了,就是这么简单粗暴。 ## 启动单个goroutine ```go func demo1() { fmt.Println("hello demo1 ~") } func main() { demo1() fmt.Println("hello world ~") } // 运行结果 // hello demo1 ~ // hello world ~ ``` 代码中 demo1 函数和其后面的打印语句是串行的。 ![](https://img.kancloud.cn/ac/62/ac62b463a022f8b7fc2d1b97fd87f01b_787x582.png) 接下来我们在调用 demo1 函数前面加上关键字go,也就是启动一个 goroutine 去执行 demo1 这个函数。 ```go func demo1() { fmt.Println("hello demo1 ~") } func main() { go demo1() fmt.Println("hello world ~") } // 运行结果 // hello world ~ ``` 这个运行结果不是预想的结果,为什么是出现这个问题呢? 其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 demo1 函数,而此时 `main goroutine` 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 `main goroutine` 也结束了,所有由 `main goroutine` 创建的 goroutine 也会一同退出(例如 `demo1 goroutine` )。 所以我们要想办法让 main 函数执行慢一点,就让 main 函数沉睡个 `500ms` 等待 `demo1 goroutrine` 结束。 ```go func demo1() { fmt.Println("hello demo1 ~") } func main() { go demo1() fmt.Println("hello world ~") time.Sleep(time.Millisecond * 500) } // 运行结果 // hello world ~ // hello demo1 ~ ``` 为什么会先打印 `hello world ~` ,然后再打印 `hello demo1 ~` 。与代码的运行顺序不同呢? 这是因为在程序中创建 goroutine 执行函数需要一定的开销,而与此同时 main 函数所在的 goroutine 是继续执行的。 ![](https://img.kancloud.cn/7f/4a/7f4a3bf410e33712a2a616e78737af72_952x654.png) 程序中有 `time.Sleep` 多少才合适呢?时间短了有些协程未执行完成,长了返回结果慢了。这里应用一个等待锁。 ```go var sw sync.WaitGroup func demo1() { // goroutine结束就登记-1 defer sw.Done() fmt.Println("hello demo1 ~") } func main() { // 启动一个goroutine就登记+1 sw.Add(1) go demo1() fmt.Println("hello world ~") // 等待所有登记的goroutine都结束 sw.Wait() } ``` ## 启动多个goroutine ```go func demo2(i int) { // goroutine结束就登记-1 defer sw.Done() fmt.Printf("value is %d\n", i) } func main() { // 使用for循环创建9个协程 for i := 1; i < 10; i++ { // 启动一个goroutine就登记+1 sw.Add(1) go demo2(i) } // 打印 hello world ~ 字符串 fmt.Println("hello world ~") // 等待所有登记的goroutine都结束 sw.Wait() } // 运行结果 // hello world ~ // value is 1 // value is 6 // value is 9 // value is 7 // value is 8 // value is 2 // value is 3 // value is 4 // value is 5 ``` >[info] 注意:创建多个goroutine执行任务,不是先创建goroutine一定先执行完成的。从上面结果可知,每次执行完的顺序都是不一样的。 ## 动态栈 操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。 ## goroutine调度 操作系统内核在调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。 区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。 在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型。 ![](https://img.kancloud.cn/e2/ac/e2ac100e700f21d8ba85c058ff62e7ab_1224x1112.png) 图片的字母的说明 - G:表示 goroutine,每执行一次go f()就创建一个 G,包含要执行的函数和上下文信息。 - 全局队列(Global Queue):存放等待运行的 G。 - P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。 - P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。 - M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。 - Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。 单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。 ## GOMAXPROCS Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8。Go语言中可以通过runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数。)