Go runtime 调度器精讲(十):异步抢占
原创文章,欢迎转载,转载请注明出处,谢谢。
0. 前言
前面介绍了运行时间过长和系统调用引起的抢占,它们都属于协作式抢占。本讲会介绍基于信号的真抢占式调度。
在介绍真抢占式调度之前看下 Go 的两种抢占式调度器:
抢占式调度器 - Go 1.2 至今
- 基于协作的抢占式调度器 - Go 1.2 - Go 1.13
改进:通过编译器在函数调用时插入抢占检查指令,在函数调用时检查当前 Goroutine 是否发起了抢占请求,实现基于协作的抢占式调度。
缺陷:Goroutine 可能会因为垃圾收集和循环长时间占用资源导致程序暂停。 - 基于信号的抢占式调度器 - Go 1.14 至今
改进:实现了基于信号的真抢占式调度。
缺陷 1:垃圾收集在扫描栈时会触发抢占式调度。
缺陷 2:抢占的时间点不够多,不能覆盖所有边缘情况。
(注:该段文字来源于 抢占式调度器)
协作式抢占是通过在函数调用时插入 抢占检查 来实现抢占的,这种抢占的问题在于,如果 goroutine 中没有函数调用,那就没有办法插入 抢占检查,导致无法抢占。我们看 Go runtime 调度器精讲(七):案例分析 的示例:
//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=1 go run main.go
程序会卡死。这是因为在 gpm 前插入 //go:nosplit 会禁止函数栈扩张,协作式抢占不能在函数栈调用前插入 抢占检查,导致这个 goroutine 没办法被抢占。
而基于信号的真抢占式调度可以改善这个问题。
1. 基于信号的真抢占式调度
这里我们说的异步抢占指的就是基于信号的真抢占式调度。
异步抢占的实现在 :
func preemptone(pp *p) bool {
...
// Request an async preemption of this P.
if preemptMSupported && debug.asyncpreemptoff == 0 {
pp.preempt = true
preemptM(mp) // 异步抢占
}
return true
}
进入 preemptM:
func preemptM(mp *m) {
...
if mp.signalPending.CompareAndSwap(0, 1) { // 更新 signalPending
signalM(mp, sigPreempt) // signalM 给线程发信号
}
...
}
// signalM sends a signal to mp.
func signalM(mp *m, sig int) {
tgkill(getpid(), int(mp.procid), sig)
}
func tgkill(tgid, tid, sig int)
调用 signalM 给线程发 sigPreempt(_SIGURG:23)信号。线程接收到该信号会做相应的处理。
1.1 线程处理抢占信号
线程是怎么处理操作系统发过来的 sigPreempt 信号的呢?
线程的信号处理在 sighandler:
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {\
// The g executing the signal handler. This is almost always
// mp.gsignal. See delayedSignal for an exception.
gsignal := getg()
mp := gsignal.m
if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal {
// Might be a preemption signal.
doSigPreempt(gp, c)
// Even if this was definitely a preemption signal, it
// may have been coalesced with another signal, so we
// still let it through to the application.
}
...
}
进入 doSigPreempt:
// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {
// Check if this G wants to be preempted and is safe to
// preempt.
if wantAsyncPreempt(gp) {
if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
// Adjust the PC and inject a call to asyncPreempt.
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
}
}
// Acknowledge the preemption.
gp.m.preemptGen.Add(1)
gp.m.signalPending.Store(0)
}
首先,doSigPreempt 调用 wantAsyncPreempt 判断是否做异步抢占:
// wantAsyncPreempt returns whether an asynchronous preemption is
// queued for gp.
func wantAsyncPreempt(gp *g) bool {
// Check both the G and the P.
return (gp.preempt || gp.m.p != 0 && gp.m.p.ptr().preempt) && readgstatus(gp)&^_Gscan == _Grunning
}
如果是,继续调用 isAsyncSafePoint 判断当前执行的是不是异步安全点,线程只有执行到异步安全点才能处理异步抢占。安全点是指 Go 运行时认为可以安全地暂停或抢占一个正在运行的 Goroutine 的位置。异步抢占的安全点确保 Goroutine 在被暂停或切换时,系统的状态是稳定和一致的,不会出现数据竞争、死锁或未完成的重要计算。
如果是异步抢占的安全点。则调用 ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc) 执行 asyncPreempt:
// asyncPreempt saves all user registers and calls asyncPreempt2.
//
// When stack scanning encounters an asyncPreempt frame, it scans that
// frame and its parent frame conservatively.
//
// asyncPreempt is implemented in assembly.
func asyncPreempt()
//go:nosplit
func asyncPreempt2() { // asyncPreempt 会调用到 asyncPreempt2
gp := getg()
gp.asyncSafePoint = true
if gp.preemptStop {
mcall(preemptPark) // 抢占类型,如果是 preemptStop 则执行 preemptPark 抢占
} else {
mcall(gopreempt_m)
}
gp.asyncSafePoint = false
}
asyncPreempt 调用 asyncPreempt2 处理 gp.preemptStop 和非 gp.preemptStop 的抢占。对于非 gp.preemptStop 的抢占,我们在 Go runtime 调度器精讲(八):运行时间过长的抢占 也介绍过,主要内容是将运行时间过长的 goroutine 放到全局队列中。接着线程执行调度获取下一个可运行的 goroutine。
1.2 案例分析
还记得在 Go runtime 调度器精讲(七):案例分析 中最后留下的思考吗?
//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
为什么开启异步抢占,程序还是会卡死?
从前面的分析结合我们的 dlv debug 发现,在安全点判断 isAsyncSafePoint 这里总是返回 false,无法进入 asyncpreempt 抢占该 goroutine。并且,由于协作式抢占的抢占点检查被 //go:nosplit 禁用了,导致协作式和异步抢占都无法抢占该 goroutine。
2. 小结
本讲介绍了异步抢占,也就是基于信号的真抢占式调度。至此,我们的 Go runtime 调度器精讲基本结束了,通过十讲内容大致理解了 Go runtime 调度器在做什么。下一讲,会总览全局,把前面讲的内容串起来。
Go runtime 调度器精讲(十):异步抢占的更多相关文章
- Python_装饰器精讲_33
from functools import wraps def wrapper(func): #func = holiday @wraps(func) def inner(*args,**kwargs ...
- Kubernetes集群调度器原理剖析及思考
简述 云环境或者计算仓库级别(将整个数据中心当做单个计算池)的集群管理系统通常会定义出工作负载的规范,并使用调度器将工作负载放置到集群恰当的位置.好的调度器可以让集群的工作处理更高效,同时提高资源利用 ...
- 不可不知的资源管理调度器Hadoop Yarn
Yarn(Yet Another Resource Negotiator)是一个资源调度平台,负责为运算程序如Spark.MapReduce分配资源和调度,不参与用户程序内部工作.同样是Master/ ...
- kubernetes调度之pod优先级和资源抢占
系列目录 Pod可以拥有优先级.优先意味着相对于其它pod某个pod更为重要.如果重要的pod不能被调度,则kubernetes调度器会优先于(驱离)低优先级的pod来让处于pending状态的高优先 ...
- 第三百五十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—scrapy信号详解
第三百五十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—scrapy信号详解 信号一般使用信号分发器dispatcher.connect(),来设置信号,和信号触发函数,当捕获到信号时执行 ...
- 第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)用Django实现搜索的自动补全功能
第三百六十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—用Django实现搜索的自动补全功能 elasticsearch(搜索引擎)提供了自动补全接口 官方说明:https://www.e ...
- 第三百六十六节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的bool组合查询
第三百六十六节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的bool组合查询 bool查询说明 filter:[],字段的过滤,不参与打分must:[] ...
- 第三百六十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的基本查询
第三百六十五节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)的基本查询 1.elasticsearch(搜索引擎)的查询 elasticsearch是功能 ...
- 第三百五十九节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)介绍以及安装
第三百五十九节,Python分布式爬虫打造搜索引擎Scrapy精讲—elasticsearch(搜索引擎)介绍以及安装 elasticsearch(搜索引擎)介绍 ElasticSearch是一个基于 ...
- 第三百五十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—将bloomfilter(布隆过滤器)集成到scrapy-redis中
第三百五十八节,Python分布式爬虫打造搜索引擎Scrapy精讲—将bloomfilter(布隆过滤器)集成到scrapy-redis中,判断URL是否重复 布隆过滤器(Bloom Filter)详 ...
随机推荐
- mysql大数据表添加字段
方案一.老表数据迁移四部曲方案1.新建老表t_order_goods的备份表t_order_goods_bak,同时加一个字段:isVirtual 并给默认值2.迁移老表t_order_goods数据 ...
- oeasy教您玩转linux 010216 随机诗词 fortunezh
我们来回顾一下 上一部分我们都讲了什么? 下载fortune 输出重定向到cowsay 多重输出重定向 fortune的细节 有没有中️文的fortune呢 # 搜索一下fortune apt sea ...
- JS实现复制粘贴图片
最近在开发公司的可视化编辑器应用, 同事们提了一个需求, 即可以直接复制图片到编辑器中粘贴, 生成对应的图片组件. 因为传统的点击上传太麻烦, 得先把图片保存到本地, 然后再回到编辑器点击上传, 选择 ...
- 学习笔记--Java中final关键字
Java中final关键字 关于Java语言中的final关键字: final是一个关键字,表示:最终的,不可改变 final修饰的类无法被继承 final修饰的方法无法被覆盖 final修饰的变量一 ...
- Java maven反应堆构建学习实践
Java maven反应堆构建学习实践 实践环境 Apache Maven 3.0.5 (Red Hat 3.0.5-17) 应用示例 示例项目结构 maven示例项目组织结构如下 maven-stu ...
- Python 函数中箭头 (->)的用处
Python 3 -> 是函数注释的一部分,表示函数返回值的类型. def useful_function(x) -> int: # Useful code, using x, here ...
- 再读vue
app.vue是项目的主组件,页面的入口文件 main.js是项目的入口文件 vue.config.js是vue-cli的配置文件//用这个配置代理,端口号 例如 const { defineConf ...
- 【Zookeeper】Re01 安装与操作
Zookeeper基于JDK开发出来的 运行环境至少需要JRE 快速安装JDK: yum install -y java-1.8.0-openjdk-devel.x86_64 # ZK镜像仓库 htt ...
- 【C3】02 操作总览
在这篇文章中,我们将会拿一个简单的HTML文档做例子,并且在上边使用CSS样式,期待你能在此过程中学会更多有关CSS的实战性知识. 前置知识 在开始本单元之前,您应该: 基本熟悉计算机操作. 基本工作 ...
- 【SpringMVC】05 RestFul风格
什么是RestFul风格? 一个资源定位和资源操作的风格,不是标准,也不是协议, 基于此风格的路径访问可以隐藏真实的参数传递,以提高网站的安全访问 以往的请求参数: jdbc:mysql://loca ...