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. CF1363A 题解

    洛谷链接&CF 链接 题目简述 共有 \(T\) 组数据. 对于每组数据,给定 \(n,x\) 和 \(n\) 个数,问是否可以从 \(n\) 个数中选 \(x\) 个使其和为奇数,可以输出 ...

  2. 数据分析应该掌握的知识及SQL技能

    一.概念及常识 1.数据分析必备的统计学知识 描述统计学 1.平均值.中位数.众数 2.方差.标准差 3.统计分布:正态分布.指数分布.二项分布.卡方分布 推论统计学 1.假设检验 2.置信区间 3. ...

  3. 修改 /etc/resolv.conf

    修改 /etc/resolv.conf /etc/resolv.conf 是 Linux 系统中用于配置 DNS 解析器的文件.确认 systemd-resolved 或 NetworkManager ...

  4. openGL之多线程渲染

    随着Vulkan的引入,我们的图形技术的发展到达了一个新的顶点,但是呢,我们的老干爹OpenGL作为落日余晖,他在一些Vulkan才有的新功能上,也提供了一些支持,现在我们来讨论一下OpenGL之多线 ...

  5. 一款.NET开源、跨平台的DASH/HLS/MSS下载工具

    前言 今天大姚给大家分享一款.NET开源(MIT License).免费.跨平台的DASH/HLS/MSS下载工具,并且支持点播和直播(DASH/HLS)的内容下载:N_m3u8DL-RE. 网络流媒 ...

  6. C# 通过反射(Reflection)调用不同名泛型方法

    概述 由于工作需要,需要通过数据类型和方法名控制方法走向 用到的数据类型有8种(string,Int16,Int32,Int64,Boolean,Byte,Single,Double) 读取的方法(参 ...

  7. 概括下为什么要用vue开发

    很多老程序员,习惯了使用原生html和js来开发前端页面,对于很多没用过vue的程序员来说,觉得没有必要. 下面,我来概括下vue的一些好处: 1.控件自动跟数据绑定,提交表单到后台的时候,可以直接使 ...

  8. 认识netty的基本组件

    Java NIO VS Netty 有了 Java NIO,而且 Netty 也是基于 Java NIO 实现,那么为什么不能直接用 Java NIO 来实现网络通信模块呢? 接下来我就给大家解释一下 ...

  9. 【郝斌C ST】指针 swap问题

    C语言 指针 swap问题 在主函数种实现变量的交换 现在我们把这交换的行为封装进方法中 swap函数确实进行了交换,打印也是10和5了,但是下面a和b的结果还是5和10 - 形参i 和 形参j 并不 ...

  10. 【Linux】11 RPM & YUM 管理工具 介绍

    rpm包的管理 介绍: 一种用于互联网下载包的打包及安装工具,它包含在某些Linux分发版中. 它生成具有.RPM扩展名的文件.RPM是RedHat Package Manager(RedHat软件包 ...