首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

Go runtime 调度器精讲(七):案例分析

编程知识
2024年09月15日 17:21

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

前面用了六讲介绍 Go runtime 调度器,这一讲我们看一个关于调度 goroutine 的程序案例分析下调度器做了什么。需要说明的是,这个程序和抢占有关,抢占目前为止还没有介绍到,如果看不懂也没有关系,有个印象就行。

1. 案例 1

执行代码:

func gpm() {
	var x int
	for {
		x++
	}
}

func main() {
	var x int
	threads := runtime.GOMAXPROCS(0)
	for i := 0; i < threads; i++ {
		go gpm()
	}

	time.Sleep(1 * time.Second)
	fmt.Println("x = ", x)
}

运行程序:

# go run main.go 
x =  0

(为什么输出 x=0 和本系列内容无关,这里直接跳过)

Go 在 1.14 版本引入了异步抢占机制,我们使用的是 1.21.0 版本的 Go,默认开启异步抢占。通过 asyncpreemptoff 标志可以开启/禁用异步抢占,asyncpreemptoff=1 表示禁用异步抢占,相应的 asyncpreemptoff=0 表示开启异步抢占。

1.1 禁用异步抢占

首先,禁用异步抢占,再次执行上述代码:

# GODEBUG=asyncpreemptoff=1 go run main.go

程序卡死,无输出。查看 CPU 使用率:

top - 10:08:53 up 86 days, 10:48,  0 users,  load average: 3.08, 1.29, 0.56
Tasks: 179 total,   2 running, 177 sleeping,   0 stopped,   0 zombie
%Cpu(s): 74.4 us,  0.6 sy,  0.0 ni, 25.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :  20074.9 total,   4279.4 free,   3118.3 used,  12677.2 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.  16781.0 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                 
1014008 root      20   0 1226288    944    668 R 293.7   0.0   5:35.81 main             // main 是执行的进程

CPU 占用率高达 293.7,太高了。

为什么会出现这样的情况呢?我们可以通过 GODEBUG=schedtrace=1000,scheddetail=1,asyncpreemptoff=1 打印程序执行的 G,P,M 信息,通过 DEBUG 输出查看调度过程中发生了什么。

当创建和线程数相等的 goroutine 后,线程执行 main goroutine。runtime(实际是 sysmon 线程,后文会讲)发现 main goroutine 运行时间过长,把它调度走,运行其它 goroutine(这是主动调度的逻辑,不属于异步抢占的范畴)。接着执行和线程数相等的 goroutine,这几个 goroutine 是永不退出的,线程会一直执行,占满逻辑核。

解决这个问题,我们改动代码如下:

func main() {
	var x int
	threads := runtime.GOMAXPROCS(0)
	for i := 0; i < threads; i++ {
		go gpm()
	}

	time.Sleep(1 * time.Nanosecond)
	fmt.Println("x = ", x)
}

因为 main goroutine 运行时间过长,被 runtime 调度走。我们把休眠时间设成 1 纳秒,不让它睡那么长。接着执行程序:

# GODEBUG=asyncpreemptoff=1 go run main.go 
x =  0

程序退出。天下武功唯快不破啊,main goroutine 直接执行完退出,不给 runtime 反应的机会。

还有其它改法吗?我们在 gpm 中加上 time.Sleep 函数调用:

func gpm() {
	var x int
	for {
		time.Sleep(1 * time.Nanosecond)
		x++
	}
}

func main() {
	var x int
	threads := runtime.GOMAXPROCS(0)
	for i := 0; i < threads; i++ {
		go gpm()
	}

	time.Sleep(1 * time.Second)
	fmt.Println("x = ", x)
}

运行程序:

# GODEBUG=asyncpreemptoff=1 go run main.go 
x =  0

也是正常退出。为什么加上函数调用就可以呢?这和抢占的逻辑有关,因为有了函数调用,就有机会在函数序言部分设置“抢占标志”,执行抢占 goroutine 的调度(同样的,后面会详细讲)。

要注意这里 time.Sleep(1 * time.Nanosecond) 加的位置,如果加在这里:

func gpm() {
	var x int
	time.Sleep(1 * time.Nanosecond)
	for {
		x++
	}
}

程序还是会卡死。

我们讨论了半天 asyncpreemptoff=1 禁止异步抢占的情况。是时候开启异步抢占看看输出结果了。

1.2 开启异步抢占

程序还是那个程序:

func gpm() {
	var x int
	for {
		x++
	}
}

func main() {
	var x int
	threads := runtime.GOMAXPROCS(0)
	for i := 0; i < threads; i++ {
		go gpm()
	}

	time.Sleep(1 * time.Second)
	fmt.Println("x = ", x)
}

开启异步抢占执行:

# GODEBUG=asyncpreemptoff=0 go run main.go 
x =  0

异步抢占就可以了,为啥异步抢占就可以了呢?异步抢占通过给线程发信号的方式,使得线程在“安全点”执行异步抢占的逻辑(后面几讲会介绍异步抢占的逻辑)。

再次改写代码如下:

//go:nosplit
func gpm() {
	var x int
	for {
		x++
	}
}

func main() {
	var x int
	threads := runtime.GOMAXPROCS(0)
	for i := 0; i < threads; i++ {
		go gpm()
	}

	time.Sleep(1 * time.Second)
	fmt.Println("x = ", x)
}

同样的执行输出:

# GODEBUG=asyncpreemptoff=0 go run main.go 

程序又卡死了...

这个程序就当思考题吧,为什么加个 //go:nosplit 程序就卡死了呢?

2. 小结

本讲不是为了凑字数,主要是为引入后续的抢占做个铺垫,下一讲会介绍运行时间过长的抢占调度。


From:https://www.cnblogs.com/xingzheanan/p/18415503
本文地址: http://www.shuzixingkong.net/article/2048
0评论
提交 加载更多评论
其他文章 代码整洁之道--读书笔记(10)
代码整洁之道 简介: 本书是编程大师“Bob 大叔”40余年编程生涯的心得体会的总结,讲解要成为真正专业的程序员需要具备什么样的态度,需要遵循什么样的原则,需要采取什么样的行动。作者以自己以及身边的同事走过的弯路、犯过的错误为例,意在为后来者引路,助其职业生涯迈上更高台阶。 本书适合所有程序员阅读,
代码整洁之道--读书笔记(10) 代码整洁之道--读书笔记(10)
Qml 实现星级评分组件 已发布
在现代应用程序中,星级评分是一个常见的用户界面元素,它允许用户对产品、服务或内容进行评价。 想必大家在用各种带有评分的软件中看到过这个组件: 本文将指导你如何使用 Qml 创建一个简单而美观的星级评分组件,并且支持高度自定义。
Qml 实现星级评分组件  已发布 Qml 实现星级评分组件  已发布 Qml 实现星级评分组件  已发布
计算机执行汇编代码的原理
计算机执行汇编代码的原理 汇编语言(Assembly Language)是一种低级编程语言,它与机器语言(Machine Language)密切相关。汇编语言由人类可读的指令构成,这些指令会被转化为机器可以理解的二进制代码,即机器码。本文将介绍计算机如何执行汇编代码的基本原理,并通过图文说明帮助理解
计算机执行汇编代码的原理 计算机执行汇编代码的原理 计算机执行汇编代码的原理
十三,Spring Boot 中注入 Servlet,Filter,Listener
十三,Spring Boot 中注入 Servlet,Filter,Listener @目录十三,Spring Boot 中注入 Servlet,Filter,Listener1. 基本介绍2. 第一种方式:使用注解方式注入:Servlet,Filter,Listener2.1 使用注解方式注入:S
十三,Spring Boot 中注入 Servlet,Filter,Listener 十三,Spring Boot 中注入 Servlet,Filter,Listener 十三,Spring Boot 中注入 Servlet,Filter,Listener
C++中对象的延迟构造
本文并不讨论“延迟初始化”或者是“懒加载的单例”那样的东西,本文要讨论的是分配某一类型所需的空间后不对类型进行构造(即对象的lifetime没有开始),更通俗点说,就是跳过对象的构造函数执行。 使用场景 我们知道,不管是定义某个类型的对象还是用operator new申请内存,对象的构造函数都是会立
C++中对象的延迟构造 C++中对象的延迟构造 C++中对象的延迟构造
Git冲突解决技巧
在多人协作的软件开发项目中,Git 冲突是不可避免的现象。当两个或更多的开发者同时修改了同一段代码,并且尝试将这些修改合并到一起时,冲突就发生了。解决这些冲突是确保代码库健康和项目顺利进行的关键。
Git冲突解决技巧
sign与unsigned的原理、数据存储与硬件的关系
目录关键字unsigned和signed数据在计算机中的存储原码 与 补码的转化与硬件关系原,反,补的原理:整型存储的本质变量存取的过程类型目前的作用十进制与二进制快速转换大小端字节序判断当前机器的字节序&quot;负零&quot;(-128)的理解截断建议在无符号类型的数值后带上u, 关键字uns
sign与unsigned的原理、数据存储与硬件的关系
AI老照片修复神器,Anole下载介绍
最近AI老照片修复上色,再一次火出圈,一些社交平台关于此话题内容流量满满,尤其是在小红书和抖音火的不得了,本期文章就来给大家分享下AI修复老照片的方式方法 本文主要介绍使用Anole修复老照片的方法,只需输入一张黑白或彩色照片,即可得到修复后的彩色结果,让往日的老照片坐上时光机重焕新生 Anole最
AI老照片修复神器,Anole下载介绍 AI老照片修复神器,Anole下载介绍 AI老照片修复神器,Anole下载介绍