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. 小结

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


Go runtime 调度器精讲(七):案例分析的更多相关文章

  1. Yarn 容量调度器多队列提交案例

    目录 Yarn 容量调度器多队列提交案例 需求 配置多队列的容量调度器 1 修改如下配置 SecureCRT的上传和下载 2 上传到集群并分发 3 重启Yarn或yarn rmadmin -refre ...

  2. 【Cocos2d-x 3.x】 调度器Scheduler类源码分析

    非个人的全部理解,部分摘自cocos官网教程,感谢cocos官网. 在<CCScheduler.h>头文件中,定义了关于调度器的五个类:Timer,TimerTargetSelector, ...

  3. Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析

    上一章,我们分析Node类的源码,在Node类里面耦合了一个 Scheduler 类的对象,这章我们就来剖析Cocos2d-x的调度器 Scheduler 类的源码,从源码中去了解它的实现与应用方法. ...

  4. 关于rt-thread调度器实现的底层代码分析

      本文使用了rt-thread自带的钩子函数和显示函数进行了实验,从rt-thread自带的延时函数rt_thread_delay()函数入手,对rt-thread系统的调度器进行分析.主要参考资料 ...

  5. JavaScript特效制作经典精讲(案例入门详解、可直接粘贴拷贝运行、史上最牛案例)

    技巧一.添加链接提示 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:// ...

  6. Python_装饰器精讲_33

    from functools import wraps def wrapper(func): #func = holiday @wraps(func) def inner(*args,**kwargs ...

  7. mybatis精讲(七)--动态sql

    目录 常用标签 if元素 choose元素 trim元素 forearch bind元素 在我们传统的开发中我们会通过拼接sql达到数据库的操作.java中的拼接不仅效率低下而且代码很长不易维护.而M ...

  8. 超精讲-逐例分析CS:LAB2-Bomb!(上)

    0. 环境要求 关于环境已经在lab1里配置过了这里要记得安装gdb 安装命令 sudo yum install gdb 实验的下载地址 http://csapp.cs.cmu.edu/3e/labs ...

  9. 超精讲-逐例分析 CSAPP:实验2-Bomb!(下)

    好了话不多说我们书接上文继续来做第二个实验下面是前半部分实验的连接 5. 第五关 首先感觉应该是个递归问题 /* Round and 'round in memory we go, where we ...

  10. Go语言goroutine调度器初始化(12)

    本文是<Go语言调度器源代码情景分析>系列的第12篇,也是第二章的第2小节. 本章将以下面这个简单的Hello World程序为例,通过跟踪其从启动到退出这一完整的运行流程来分析Go语言调 ...

随机推荐

  1. Mysql-explain之Using temporary和Using filesort解决方案

    第一条语句 explainselect * from tb_wm_shop where is_delete != 1 and is_authentication = 1 ORDER BY create ...

  2. SQL Server 截取字符串

    select top 100 substring(qr_code,8,8) ,* from [LiuJun_PKh_lcfc_hf] --where right(ri,8) or substring( ...

  3. 安装jieba中文分词库

    插入一条: 有个更快安装下载jieba的方法,用镜像下载,非常快,2秒就行 pip install jieba -i https://pypi.douban.com/simple/ 1.打开官方网站: ...

  4. [ABC363G] Dynamic Scheduling 与 P4511 [CTSC2015] 日程管理

    思路: 对于插入操作,设插入 \(\{t,p\}\): 若当前 \(1 \sim t\) 有空位,那么就放进去. 否则,\(1 \sim t\) 是被塞满了的: 首先容易想到的是找到 \(1 \sim ...

  5. h5py文件写入之——flush和update

    技术背景 在前面的一篇博客中,我们介绍过使用VMD可视化H5MD标准化格式的轨迹文件的方法.H5MD本质上就是一个有规范格式的hdf5二进制文件,本文主要介绍两个关于hdf5的内容更新操作. 写入和更 ...

  6. RPC接口测试(六)RPC协议解析(重要!重要!重要!)

    RPC协议解析 RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.简言之,RPC使得程 ...

  7. Jmeter函数助手22-V

    V函数用于执行变量名.嵌套函数.类似eval函数 Name of variable (may include variable and function references):必填,填入变量名称或者 ...

  8. LinuxUDP通讯

    目录 前言 一.UDP通讯 1.UDP通讯概述 2.UDP的特点 3.UDP的应用 二.UDP基本通讯 1.socket函数 2.bind函数 2.1 主机字节序和网络字节序 2.2 点分制十进制转换 ...

  9. stable diffusion 实践与测试

    stable diffusion 实践与测试 放大 原图高清放大 原始图片 当不满意图片质量的时候 使用stable diffusion进行二次处理 选择适合图片风格的模型,再次根据图片写出提示词 输 ...

  10. 【Java】讲讲StreamAPI

    预设场景: 从Mybatis调用Mapper得到的用户集合 List<UserDTO> userList = new ArrayList<>(); 常用的几种API用法示例: ...