文章目录
- Goroutine
- goroutine的创建
- GPM介绍
- goroutine调度
道阻且长,行则将至,行而不辍,未来可期🌟。人生是一条且漫长且充满荆棘的道路,一路上充斥着各种欲望与诱惑,不断学习,不断修炼,不悔昨日,不畏将来!
希望这篇能够带给你们或多或少的帮助,诸位!顶峰见 🚀
Goroutine
- Golang中最迷人的一个优点就是从语言层面就支持并发
- 在Golang中的goroutine(协程)类似于其他语言的线程
我们可以理解goroutine是一个轻量级线程,所占用的栈空间很小(一般为2KB)且可以扩容与减小
goroutine的创建
在Go语言内可以通过go func
来创建一个goroutine
package main
func hello() {
fmt.Println("Hello")
}
func main() {
go hello()
fmt.Println("Main")
}
Go程序开始时会创建一个 main goroutine。我们通过go关键字也会创建一个goroutine去执行hello程序,而此时main goroutine会继续执行。
此时程序里面会有两个goroutine并发执行,当main函数结束后 main goroutine会结束,由main goroutine创建的其它goroutine也会立即结束
所以这里hello是有可能打印不出来的
执行结果:
Main
所以为了避免main函数代码运行结束,而其它goroutine仍在运行中,所以我们需要main函数阻塞等待其它goroutine执行结束
暴力等待可通过time.Sleep
。这里使用sync
模块,利用计数等待实现
package main
import (
"fmt"
"sync"
)
var ws sync.WaitGroup
func hello() {
// goroutine函数结束后,Done表示计数减1
defer ws.Done()
fmt.Println("Hello")
}
func main() {
// 开启一个goroutine之前进行计数
ws.Add(1)
go hello()
fmt.Println("Main")
// 当计数为0的时候才结束,否则阻塞等待
ws.Wait()
}
执行结果:
Main
Hello
GPM介绍
操作系统的线程一般都有固定的栈(通常为2MB),而Go语言中的goroutine非常轻量级,一个goroutine的初始栈空间很小(一般为2KB),并且goroutine的栈空间大小不是固定的,通常可以根据内容进行扩容增大或减小,Go的runtime会自动分配合适的goroutine的栈空间。
由于线程间切换需要进行一个完整的上下文切换过程开销较大,Go语言本身具有一套调度goroutine的系统。它将按照一定规则将goroutine调度到操作系统线程上执行,经过数个版本的迭代,Go语言调度器目前按照GPM模型
-
G:表示goroutine,通过go func关键字创建,包含执行的函数和上下文信息
全局队列:存放等待运行的G
-
P:表示goroutine执行所需要的资源,最多有GOMAXPROCS个(CPU的核心)
P本地队列:也是存放等待运行G的地方,创建G之后会优先放到P的队列,不过P队列存放的G数量有限,不超过256。如果P本地队列满了则会批量移动部分G到全局队列
-
M:线程想运行任务就得获取P。从P本地队列获取G,如果P队列为空,则尝试从其它的P队列获取都没有的话则尝试从全局队列获取。M运行G,G执行之后,M又会继续获取G,不断重复
goroutine调度器和操作调度器是通过M结合的,每个M代表1个内核线程,操作系统调度器负责把内核线程分配到CPU核上运行
从线程来讲:Go语言的goroutine相较于其它语言开启线程的优势在于,其它线程的线程是由内核来调度的,goroutine则是Go语言运行时自己调度的,完全是在用户态下面完成的,不涉及内核与用户态之间频繁切换
goroutine调度
goroutine的执行顺序并不是按照创建时间来的,而是取决于哪个G优先被M执行
猜测该函数执行后的打印结果:
func f() {
ws.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
ws.Done()
}()
}
}
执行结果:
5
5
5
5
5
程序解析:
go关键字会创建goroutine去执行里面的匿名函数,而从创建->执行中间需要一定的时间,而for循环却在继续,匿名函数里面访问的是外部变量i,所以在实际执行到这个goroutine的时候,循环可能已经结束了,而i的值最终会变成5。所以大部分情况会打印5 5 5 5 5。也可能存在goroutine执行较快的情况,某个goroutine在循环未结束之前就执行了,可能会打印出其它数字。
猜测该函数执行后的打印结果:
func f2() {
ws.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
fmt.Println(j)
ws.Done()
}(i)
}
程序解析:
该函数与f不同的是,i由于传递到了匿名函数里面,那么打印的值是确认的,而由于goroutine并不是按顺序执行的,所以最终打印的结果就是:所有数字都会打印出来,只是顺序不同