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

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

Go runtime 调度器精讲(十):异步抢占

编程知识
2024年09月16日 19:20

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


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 调度器在做什么。下一讲,会总览全局,把前面讲的内容串起来。


From:https://www.cnblogs.com/xingzheanan/p/18416290
本文地址: http://www.shuzixingkong.net/article/2067
0评论
提交 加载更多评论
其他文章 mongo集群同步数据异常,手动同步节点副本数据
转载请注明出处: 数据同步方案 当副本集节点的复制进程落后太多,以至于主节点覆盖了该节点尚未复制的 oplog 条目时,副本集节点就会变为“陈旧”。节点跟不上,就会变得“陈旧”。出现这种情况时,必须删除副本集节点的数据,然后执行初始同步,从而完全重新同步该节点。 MongoDB 提供了两种执行初始同
mongo集群同步数据异常,手动同步节点副本数据
四类取整方式
目录C语言的四种取整方式:零向取整trunc函数(C99)trunc的使用地板取整floor函数的使用向上取整ceil函数的使用四舍五入round函数(C99)round函数的使用四种取整方式演示 C语言的四种取整方式: 零向取整 如图: 可以发现C语言a和b的取整方式都不是四舍五入,而是直接舍弃小
四类取整方式 四类取整方式 四类取整方式
如何基于Java解析国密数字证书
一、说明 随着信息安全的重要性日益凸显,数字证书在各种安全通信场景中扮演着至关重要的角色。国密算法,作为我国自主研发的加密算法标准,其应用也愈发广泛。然而,在Java环境中解析使用国密算法的数字证书时,我们可能会遇到一些挑战。 本文主要分享如何在 Java 中解析采用 SM3WITHSM2 签发算法
如何基于Java解析国密数字证书 如何基于Java解析国密数字证书
nRF24L01芯片驱动记录
nRF24L01芯片驱动记录 ​ 学习完了usb,了解了部分元器件的功能以及用途后,打算在端午假期用一天的时间完成一个小目标,不过实际上是花了一天半才成功实现,现将驱动nRF24L01芯片的整个过程记录下来。 小目标 驱动nRF24L01芯片,实现nRF24L01芯片之间的通讯 在淘宝问客服找驱动代
LeetCode题集-4 - 寻找两个有序数组的中位数,图文并茂,六种解法,万字讲解
题目:给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 算法的时间复杂度应该为 O(log (m+n)) 。 作为目前遇到的第一个困难级别题目,我感觉这题还是挺难的,研究了三天总算研究明白了,下面就给大家分享一下这题的几种
LeetCode题集-4 - 寻找两个有序数组的中位数,图文并茂,六种解法,万字讲解 LeetCode题集-4 - 寻找两个有序数组的中位数,图文并茂,六种解法,万字讲解 LeetCode题集-4 - 寻找两个有序数组的中位数,图文并茂,六种解法,万字讲解
Java读取寄存器数据的方法
本文简要介绍了在Java中直接读取硬件寄存器(如CPU寄存器、I/O端口等)通常不是一个直接的任务,因为Java设计之初就是为了跨平台的安全性和易用性,它并不直接提供访问底层硬件的API。不过,在嵌入式系统、工业控制或需要直接与硬件交互的特定场景中,可能会使用JNI(Java Native Inte
基于Tauri2+Vue3搭建桌面端程序|tauri2+vite5多窗口|消息提醒|托盘闪烁
基于tauri2+vite5+vue3封装多窗口实践|自定义消息提醒|托盘右键菜单及图标闪烁 这段时间一直在捣鼓最新版Tauri2.x整合Vite5搭建桌面端多开窗体应用实践。tauri2.0相较于1.0版本api有了比较多的更改,而且tauri2支持创建android/ios应用。至于具体的api
基于Tauri2+Vue3搭建桌面端程序|tauri2+vite5多窗口|消息提醒|托盘闪烁 基于Tauri2+Vue3搭建桌面端程序|tauri2+vite5多窗口|消息提醒|托盘闪烁 基于Tauri2+Vue3搭建桌面端程序|tauri2+vite5多窗口|消息提醒|托盘闪烁
ComfyUI 基础教程(五) —— 应用 IP-Adapter 实现图像风格迁移
中秋假期,又可以玩玩 AI 了。前面介绍了 ComfyUI 的 Lora 模型以及 ControlNet,本文介绍另一个非常重要且使用的节点,IP-Adapter。 一、 IP-Adapter 概念 1.1 IPAdapter 的介绍 IP-Adapter 的是腾讯 ailab 实验室发布的一个 S
ComfyUI 基础教程(五) —— 应用 IP-Adapter 实现图像风格迁移 ComfyUI 基础教程(五) —— 应用 IP-Adapter 实现图像风格迁移 ComfyUI 基础教程(五) —— 应用 IP-Adapter 实现图像风格迁移