社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
并发执行各种线程,切换线程会造成较大的性能损耗,多线程的同步竞争(锁、竞争资源冲突等)问题,最后还有上面说到的占用内存较大。
系统分为内核空间和用户空间,可以通过这个思想,将线程分割开来作为一个个协程co-routine,内核空间存放内核线程,用户空间存放切割后的协程,中间通过协程调度器来维持工作。
所以说,M:N的调度工作就交给了协程调度器,线程层面(即内核空间)语言控制不了,所以说语言间的区别主要在于协程调度器的设计。
Golang将co-routine封装,自己命名为Goroutine,内存占用个位数KB,这样就可以大量产生自己想要的协程。
Golang编写自己的调度器,使得调度更加灵活,所以GPM出现。
func main() {
// 创建trace文件
traceFile, err := os.Create("trace.out")
if err != nil {
log.Fatalln(err.Error())
return
}
defer traceFile.Close()
// 启动trace
err = trace.Start(traceFile)
if err != nil {
log.Fatalln(err.Error())
return
}
// 逻辑代码
fmt.Println("Hello GPM~")
// 停止trace
trace.Stop()
}
编译运行后会得到一个trace.out文件,然后通过go tool trace工具可以分析trace文件,返回一个url通过浏览器可视化,如下图。
局部性原则:由一个G1创建一个新的G3时,优先将新的G3放入和G1相同的本地队列中。
当本地队列一个G执行完毕后,先切换为G0,然后通过G0先去调用本地队列中的G任务。
如果一个G申请创建过多的G,超过了本地队列的最大值,根据创建G的场景,会先依次将本地队列存满,这时本地队列已满,但还有新创建G的需求,比如G8,那么就会将本地队列一分为二,将前一部分的移入全局队列,后一部分往前移 ,并将G8也移入全局队列,此时已有空闲空间,若还有新的G申请创建,则正常放入本地队列即可,循环反复。
每次新创建一个G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。假定唤醒了M2,M2绑定了P2,此时M2会运行自身的G0,但P2队列本身没有G任务,那么M2此时就为自旋线程(没有G任务,但是在运行状态的线程,会不断地寻找G,先从全局G队列中拿取,全局队列为空时,再去别的进程偷取,并且为批量偷取,一分为二,将后半部分全部偷取,总共自旋线程获取数量n符合如下公式)。n = min(len(GQ) / GOMAXPROCS + 1, len(GQ / 2))
假设当前M1和M2在工作,M3和M4为自旋线程,M5和M6为休眠队列中的休眠线程,即最大线程数为4。当前M2对应的P2中的G1发生系统调用/阻塞且队列中还有G2任务,则M2和P2立即解绑,P2会执行以下判断:如果存在P2本地队列中还有G任务或全局G队列中有G任务或休眠线程队列中有空闲的M,则P2都会立马唤醒一个M与其绑定,否则P2则会加入到空闲P列表,等待M来获取可用的P。上例中,P2本地队列中还有G2任务,则可以与空闲线程队列中的M5进行绑定,继续后续工作。
接上例,若G1从阻塞变为非阻塞,此时对应的M2会优先尝试获取原先的P2,若P2已被绑定在别的M,则从空闲P队列中获取,若空闲队列中为空,则M2获取P失败,直接将M2放入休眠线程队列,G变为可运行状态,放入全局G队列中。
————————————————
版权声明:本文为CSDN博主「Pekue」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Pekue/article/details/116237421
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!