当前位置: 首页 > news >正文

【Golang 面试 - 进阶题】每日 3 题(十三)

✍个人博客:Pandaconda-CSDN博客

📣专栏地址:http://t.csdnimg.cn/UWz06

📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪

37. GM P 中 hand off 机制

GMP 中的 hand off 机制是指在某个 M 线程需要将当前正在执行的 Goroutine 交给另一个 M 线程时,使用的一种机制。

具体地,hand off 机制的实现过程如下:

当一个 M 线程需要将当前正在执行的 Goroutine 交给另一个 M 线程时,它会将该 Goroutine 和一个指向目标 M 线程的指针打包成一个结构体,称为 hand off 对象。

当目标 M 线程的本地队列中没有 Goroutine 可供执行时,它会从全局队列中获取一个 hand off 对象,并尝试将其中的 Goroutine 从原来的 M 线程中获取出来,添加到自己的本地队列中执行。在此期间,当前 M 线程会不断尝试从全局队列中获取 Goroutine 并将其调度到本地队列中执行。

当目标 M 线程成功获取到 hand off 对象后,它会将其中的 Goroutine 添加到自己的本地队列中,并将它们调度到绑定的 P 上执行。

hand off 机制的好处是可以避免线程饥饿,提高 Goroutine 的调度效率。当一个 M 线程需要将当前正在执行的 Goroutine 交给另一个 M 线程时,可以使用 hand off 机制来尽快地将 Goroutine 交给目标 M 线程,从而避免线程饥饿的问题。同时,由于 hand off 机制只在需要将当前正在执行的 Goroutine 交给另一个 M 线程时才会被使用,因此相对于 work stealing 机制来说,它的实现比较简单,不会增加太多额外的开销。

示例

由于 hand off 机制的使用场景比较特殊,且需要涉及到多个 Goroutine 之间的交互,因此比较难以直接演示。

不过,我们可以通过一个简单的示例来说明 hand off 机制的基本使用方法和效果。

假设我们有一个生产者-消费者模型,其中有多个生产者 Goroutine 和多个消费者 Goroutine,它们都需要不断地从一个共享的队列中获取任务进行处理。为了提高并发效率,我们可以使用 GMP 模型来对任务进行调度。

在这个示例中,我们使用一个全局队列来存储任务,并使用 hand off 机制来将任务从一个 M 线程转移到另一个 M 线程。每个生产者 Goroutine 和消费者 Goroutine 都会不断地尝试从全局队列中获取任务,并将其添加到自己的本地队列中执行。当某个 Goroutine 的本地队列为空时,它会从全局队列中获取一个 hand off 对象,并将其中的 Goroutine 从原来的 M 线程中获取出来,添加到自己的本地队列中执行。在此期间,其他 Goroutine 也可以从全局队列中获取任务,并将其添加到自己的本地队列中执行。

示例代码如下:

package main
import ("fmt""sync""time"
)
// 全局变量,用于保存正在处理的任务
var currentTask int
func producer(tasks chan<- int, wg *sync.WaitGroup) {defer wg.Done()// 生产 10 个任务for i := 1; i <= 10; i++ {fmt.Printf("producer producing task %d\n", i)tasks <- itime.Sleep(time.Second)}// 关闭任务通道close(tasks)
}
func consumer(id int, tasks <-chan int, done chan<- bool, wg *sync.WaitGroup) {defer wg.Done()for task := range tasks {fmt.Printf("consumer %d processing task %d\n", id, task)// 模拟处理任务的耗时time.Sleep(time.Second)// 交出任务,使用 hand off 机制currentTask = taskdone <- true}fmt.Printf("consumer %d has processed all tasks\n", id)
}
func main() {var wg sync.WaitGroup// 任务通道tasks := make(chan int)// done 通道,用于实现 hand off 机制done := make(chan bool)// 启动 3 个 consumer goroutinefor i := 1; i <= 3; i++ {wg.Add(1)go consumer(i, tasks, done, &wg)}// 启动 producer goroutinewg.Add(1)go producer(tasks, &wg)// 等待所有 goroutine 执行完毕wg.Wait()// 所有任务处理完毕后,输出最后一个交出任务的 consumer ID 和任务 IDfmt.Printf("last consumer to hand off task: %d, task ID: %d\n", currentTask%3+1, currentTask)
}

在这个示例中,我们定义了一个全局变量 currentTask,用于保存当前正在处理的任务。在 consumer goroutine 中,当处理完一个任务后,使用 hand off 机制将任务交出,并更新 currentTask 的值。在程序结束时,我们可以通过输出 currentTask 的值来查看最后一个交出任务的 consumer ID 和任务 ID。

38. Go 抢 占式调度?

在 1.2 版本之前,Go 的调度器仍然不支持抢占式调度,程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度,这会引发一些问题,比如:

  • 某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿。

  • 垃圾回收器是需要 stop the world 的,如果垃圾回收器想要运行了,那么它必须先通知其它的 goroutine 停下来,这会造成较长时间的等待时间。

为解决这个问题:

  • Go 1.2 中实现了基于协作的“抢占式”调度。

  • Go 1.14 中实现了基于信号的“抢占式”调度。

基于协作的抢占式调度

协作式:大家都按事先定义好的规则来,比如:一个 goroutine 执行完后,退出,让出 p,然后下一个 goroutine 被调度到 p 上运行。这样做的缺点就在于是否让出 p 的决定权在 groutine 自身。一旦某个 g 不主动让出 p 或执行时间较长,那么后面的 goroutine 只能等着,没有方法让前者让出 p,导致延迟甚至饿死。

非协作式: 就是由 runtime 来决定一个 goroutine 运行多长时间,如果你不主动让出,对不起,我有手段可以抢占你,把你踢出去,让后面的 goroutine 进来运行。

基于协作的抢占式调度流程:

  • 编译器会在调用函数前插入 runtime.morestack,让运行时有机会在这段代码中检查是否需要执行抢占调度。

  • Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms,那么会在这个协程设置一个抢占标记。

  • 当发生函数调用时,可能会执行编译器插入的 runtime.morestack,它调用的 runtime.newstack 会检查抢占标记,如果有抢占标记就会触发抢占让出 cpu,切到调度主协程里。

这种解决方案只能说局部解决了 “饿死” 问题,只在有函数调用的地方才能插入 “抢占” 代码(埋点),对于没有函数调用而是纯算法循环计算的 G,Go 调度器依然无法抢占。

比如,死循环等并没有给编译器插入抢占代码的机会,以下程序在 go 1.14 之前的 go 版本中,运行后会一直卡住,而不会打印 I got scheduled!

package main
import ("fmt""runtime""time"
)
func main() {runtime.GOMAXPROCS(1)go func() {for {}}()time.Sleep(time.Second)fmt.Println("I got scheduled!")
}

为了解决这些问题,Go 在 1.14 版本中增加了对非协作的抢占式调度的支持,这种抢占式调度是基于系统信号的,也就是通过向线程发送信号的方式来抢占正在运行的 Goroutine。

基于信号的抢占式调度

真正的抢占式调度是基于信号完成的,所以也称为 “异步抢占”。不管协程有没有意愿主动让出 cpu 运行权,只要某个协程执行时间过长,就会发送信号强行夺取 cpu 运行权。

  • M 注册一个 SIGURG 信号的处理函数:sighandler。

  • sysmon 启动后会间隔性的进行监控,最长间隔 10ms,最短间隔 20us。如果发现某协程独占 P 超过 10ms,会给 M 发送抢占信号。

  • M 收到信号后,内核执行 sighandler 函数把当前协程的状态从 _Grunning 正在执行改成 _Grunnable 可执行,把抢占的协程放到全局队列里,M 继续寻找其他 goroutine 来运行。

  • 被抢占的 G 再次调度过来执行时,会继续原来的执行流。

抢占分为 _Prunning_Psyscall_Psyscall 抢占通常是由于阻塞性系统调用引起的,比如磁盘 io、cgo。_Prunning 抢占通常是由于一些类似死循环的计算逻辑引起的。

39. 协作式 的抢占式调度

在 Go 语言中,Goroutine 调度器采用的是协作式调度,也就是说,在一个 Goroutine 执行过程中,如果没有主动交出控制权(比如调用 time.Sleep()、channel 操作等),其他 Goroutine 是无法抢占执行的。这样可以避免出现线程安全的问题,但也会导致某个 Goroutine 长时间占用 CPU 时间,从而降低程序整体的并发性能。

为了解决这个问题,Go 语言在 1.14 版本引入了抢占式调度。抢占式调度的主要思想是,在 Goroutine 执行过程中,如果某个 Goroutine 执行时间过长,会被强制抢占,让其他 Goroutine 有机会执行。这样可以保证所有 Goroutine 公平地获得 CPU 时间,从而提高程序的并发性能。

在抢占式调度中,Go 语言采用了基于信号的抢占方式。具体来说,当一个 Goroutine 执行时间过长时,会在指定时间内收到一个抢占信号,然后在信号处理程序中暂停当前 Goroutine 的执行,并将控制权交给调度器,让调度器决定下一个要执行的 Goroutine。当下一个 Goroutine 开始执行时,之前被暂停的 Goroutine 就被称为 “被抢占” 的 Goroutine。

需要注意的是,抢占式调度只在 Go 语言的系统线程中生效,而在非系统线程中,仍然采用协作式调度。这是因为非系统线程是由 Go 语言运行时管理的,无法被操作系统直接抢占,因此只能采用协作式调度。另外,抢占式调度对于需要实现低延迟的应用程序可能不太适合,因为抢占操作需要额外的 CPU 时间,从而增加了系统的响应时间。

实例演示

下面是一个简单的抢占式调度的示例代码:

package main
import ("fmt""time"
)
func main() {go func() {for {fmt.Println("Goroutine 1 is running")time.Sleep(time.Second)}}()go func() {for {fmt.Println("Goroutine 2 is running")time.Sleep(time.Second)}}()for {fmt.Println("Main Goroutine is running")time.Sleep(time.Second)}
}

在这个示例代码中,我们定义了三个 Goroutine,分别是 “Goroutine 1”、“Goroutine 2” 和 “Main Goroutine”。其中,“Goroutine 1” 和 “Goroutine 2” 分别每秒输出一次自己的名称,而 “Main Goroutine” 每秒输出一次自己的名称。

我们可以运行这个程序并观察输出结果。在协作式调度下,我们会发现 “Main Goroutine” 总是先输出,而 “Goroutine 1” 和 “Goroutine 2” 则交替输出。而在抢占式调度下,由于 Goroutine 执行时间被限制,我们会发现 “Main Goroutine”、“Goroutine 1” 和 “Goroutine 2” 三个 Goroutine 的输出基本上是随机的,每个 Goroutine 每秒只能输出一次。

需要注意的是,抢占式调度并不是默认启用的,如果要启用抢占式调度,可以通过设置 GOMAXPROCS 环境变量或调用 runtime.GOMAXPROCS() 函数来指定使用的系统线程数。当 GOMAXPROCS 的值大于 1 时,Go 语言会自动启用抢占式调度。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 高通8255 Android Virtio Virtio-IIC 配置方法
  • WPF学习(2)-UniformGrid控件(均分布局)+StackPanel控件(栈式布局)
  • 优秀的行为验证码的应用场景与行业案例
  • rancher v2.4.17安装部署并授权永久使用
  • 动手学深度学习V2每日笔记(经典卷积神经网络LeNet)
  • 0205其它新型电力电子器件
  • C语言 | Leetcode C语言题解之第324题摆动排序II
  • word加密文档忘记密码要如何打开
  • Linux的目录文件函数接口,链接文件函数接口,获得文件详细信息
  • 【嵌入式】RTOS和Linux的区别
  • ios如何动态添加控件及动画
  • Unity补完计划 之 必须学会的Tile拓展内容(新增瓦片)
  • 关于地址的级联选择器
  • 宝塔nginx安装geoip2
  • iOS弱引用
  • 03Go 类型总结
  • Angularjs之国际化
  • canvas 五子棋游戏
  • Docker入门(二) - Dockerfile
  • IDEA常用插件整理
  • java正则表式的使用
  • js 实现textarea输入字数提示
  • LeetCode算法系列_0891_子序列宽度之和
  • linux安装openssl、swoole等扩展的具体步骤
  • PAT A1017 优先队列
  • Redis 懒删除(lazy free)简史
  • Vue2 SSR 的优化之旅
  • Web Storage相关
  • 大主子表关联的性能优化方法
  • 思考 CSS 架构
  • 曜石科技宣布获得千万级天使轮投资,全方面布局电竞产业链 ...
  • ​html.parser --- 简单的 HTML 和 XHTML 解析器​
  • ​Kaggle X光肺炎检测比赛第二名方案解析 | CVPR 2020 Workshop
  • ​如何使用QGIS制作三维建筑
  • #数据结构 笔记三
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (14)Hive调优——合并小文件
  • (2024,RWKV-5/6,RNN,矩阵值注意力状态,数据依赖线性插值,LoRA,多语言分词器)Eagle 和 Finch
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (C#)获取字符编码的类
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (初研) Sentence-embedding fine-tune notebook
  • (二)WCF的Binding模型
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (七)c52学习之旅-中断
  • (生成器)yield与(迭代器)generator
  • (十六)视图变换 正交投影 透视投影
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • (算法)大数的进制转换
  • (算法二)滑动窗口
  • (贪心 + 双指针) LeetCode 455. 分发饼干
  • (一)VirtualBox安装增强功能
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • .Net Core 生成管理员权限的应用程序
  • .Net Core缓存组件(MemoryCache)源码解析