✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06
📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
40. 基 于信号的抢占式调度
在 golang 中,除了协作式调度和抢占式调度,还有一种基于信号的抢占式调度。基于信号的抢占式调度可以让 Goroutine 在执行过程中被立即中断,并强制切换到其他 Goroutine,从而实现抢占式调度。
在 golang 中,我们可以使用 runtime 包中的两个函数实现基于信号的抢占式调度:
-
runtime.Gosched():让出 CPU 时间片,让其他 Goroutine 运行。
-
runtime.LockOSThread():将当前 Goroutine 绑定到当前线程上,让该 Goroutine 独占一个线程,从而实现更精细的调度控制。
下面是一个简单的基于信号的抢占式调度的示例代码:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
for {
fmt.Println("Goroutine 1 is running")
runtime.Gosched()
}
}()
go func() {
for {
fmt.Println("Goroutine 2 is running")
runtime.Gosched()
}
}()
for {
fmt.Println("Main Goroutine is running")
time.Sleep(time.Second)
}
}
在这个示例代码中,我们定义了三个 Goroutine,分别是“Goroutine 1”、“Goroutine 2”和“Main Goroutine”。其中,“Goroutine 1”和“Goroutine 2”分别不断输出自己的名称,并在每次输出后使用 runtime.Gosched() 函数让出 CPU 时间片,从而实现抢占式调度。而“Main Goroutine”每秒输出一次自己的名称,并使用 time.Sleep() 函数暂停一秒钟,从而实现协作式调度。
需要注意的是,基于信号的抢占式调度不适用于所有场景,因为频繁调用 runtime.Gosched() 函数会导致性能下降,应该根据实际需求进行选择。
41. Go 如何查 看运行时调度信息?
有 2 种方式可以查看一个程序的调度 GMP 信息,分别是 go tool trace 和 GODEBUG。
trace.go
package main
import (
"fmt"
"os"
"runtime/trace"
"time"
)
func main() {
//创建trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
//启动trace goroutine
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
//main
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("Hello World")
}
}
go tool trace
启动可视化界面:
go run trace.go
go tool trace trace.out
2022/04/22 10:44:11 Parsing trace...
2022/04/22 10:44:11 Splitting trace...
2022/04/22 10:44:11 Opening browser. Trace viewer is listening on http://127.0.0.1:35488
打开 http://127.0.0.1:35488
查看可视化界面:
点击 view trace
能够看见可视化的调度流程:
一共有 2 个 G 在程序中,一个是特殊的 G0,是每个 M 必须有的一个初始化的 G,另外一个是 G1 main goroutine (执行 main 函数的协程),在一段时间内处于可运行和运行的状态。
1. 点击 Thr eads 那一行可视化的数据条,我们会看到 M 详细的信息
一共有 2 个 M 在程序中,一个是特殊的 M0,用于初始化使用,另外一个是用于执行 G1 的 M1。
2. 点击 Proc 那一行可视化的数据条,我们会看到 P 上正在运行 goroutine 详细的信息
一共有 3 个 P 在程序中,分别是 P0、P1、P2。
点击具体的 Goroutine 行为后可以看到其相关联的详细信息:
Start:开始时间
Wall Duration:持续时间
Self Time:执行时间
Start Stack Trace:开始时的堆栈信息
End Stack Trace:结束时的堆栈信息
Incoming flow:输入流
Outgoing flow:输出流
Preceding events:之前的事件
Following events:之后的事件
All connected:所有连接的事件
GODEBUG
GODEBUG 变量可以控制运行时内的调试变量。查看调度器信息,将会使用如下两个参数:
-
schedtrace:设置
schedtrace=X
参数可以使运行时在每 X 毫秒发出一行调度器的摘要信息到标准 err 输出中。 -
scheddetail:设置
schedtrace=X
和scheddetail=1
可以使运行时在每 X 毫秒发出一次详细的多行信息,信息内容主要包括调度程序、处理器、OS 线程 和 Goroutine 的状态。
查看基本信息
go build trace.go
GODEBUG=schedtrace=1000 ./trace
SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
Hello World
SCHED 1010ms: gomaxprocs=8 idleprocs=8 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0]
Hello World
SCHED 2014ms: gomaxprocs=8 idleprocs=8 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0]
Hello World
SCHED 3024ms: gomaxprocs=8 idleprocs=8 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0]
Hello World
SCHED 4027ms: gomaxprocs=8 idleprocs=8 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0]
Hello World
SCHED 5029ms: gomaxprocs=8 idleprocs=7 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0 0 0 0 0]
sched:每一行都代表调度器的调试信息,后面提示的毫秒数表示启动到现在的运行时间,输出的时间间隔受 schedtrace
的值影响。
gomaxprocs:当前的 CPU 核心数(GOMAXPROCS 的当前值)。
idleprocs:空闲的处理器数量,后面的数字表示当前的空闲数量。
threads:OS 线程数量,后面的数字表示当前正在运行的线程数量。
spinningthreads:自旋状态的 OS 线程数量。
idlethreads:空闲的线程数量。
runqueue:全局队列中中的 Goroutine 数量,而后面的[0 0 0 0 0 0 0 0] 则分别代表这 8 个 P 的本地队列正在运行的 Goroutine 数量。
查看详细信息
go build trace.go
GODEBUG=scheddetail=1,schedtrace=1000 ./trace
SCHED 0ms: gomaxprocs=8 idleprocs=6 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
P0: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=1 gfreecnt=0 timerslen=0
P1: status=1 schedtick=0 syscalltick=0 m=2 runqsize=0 gfreecnt=0 timerslen=0
P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
P4: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
P5: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
P6: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
P7: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 timerslen=0
M3: p=0 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
M2: p=1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 spinning=false blocked=false lockedg=-1
M0: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=1
G1: status=1(chan receive) m=-1 lockedm=0
G2: status=1() m=-1 lockedm=-1
G3: status=1() m=-1 lockedm=-1
G4: status=4(GC scavenge wait) m=-1 lockedm=-1
G
status:G 的运行状态。
m:隶属哪一个 M。
lockedm:是否有锁定 M。
G 的运行状态共涉及如下 9 种状态:
状态 | 值 | 含义 |
_Gidle | 0 | 刚刚被分配,还没有进行初始化。 |
_Grunnable | 1 | 已经在运行队列中,还没有执行用户代码。 |
_Grunning | 2 | 不在运行队列里中,已经可以执行用户代码,此时已经分配了 M 和 P。 |
_Gsyscall | 3 | 正在执行系统调用,此时分配了 M。 |
_Gwaiting | 4 | 在运行时被阻止,没有执行用户代码,也不在运行队列中,此时它正在某处阻塞等待中。 |
_Gmoribund_unused | 5 | 尚未使用,但是在 gdb 中进行了硬编码。 |
_Gdead | 6 | 尚未使用,这个状态可能是刚退出或是刚被初始化,此时它并没有执行用户代码,有可能有也有可能没有分配堆栈。 |
_Genqueue_unused | 7 | 尚未使用。 |
_Gcopystack | 8 | 正在复制堆栈,并没有执行用户代码,也不在运行队列中。 |
M
p:隶属哪一个 P。
curg:当前正在使用哪个 G。
runqsize:运行队列中的 G 数量。
gfreecnt:可用的G(状态为 Gdead)。
mallocing:是否正在分配内存。
throwing:是否抛出异常。
preemptoff:不等于空字符串的话,保持 curg 在这个 m 上运行。
P
status:P 的运行状态。
schedtick:P 的调度次数。
syscalltick:P 的系统调用次数。
m:隶属哪一个 M。
runqsize:运行队列中的 G 数量。
gfreecnt:可用的G(状态为 Gdead)
状态 | 值 | 含义 |
_Pidle | 0 | 刚刚被分配,还没有进行进行初始化。 |
_Prunning | 1 | 当 M 与 P 绑定调用 acquirep 时,P 的状态会改变为 _Prunning。 |
_Psyscall | 2 | 正在执行系统调用。 |
_Pgcstop | 3 | 暂停运行,此时系统正在进行 GC,直至 GC 结束后才会转变到下一个状态阶段。 |
_Pdead | 4 | 废弃,不再使用。 |
42. GMP 调度过程中存在哪些阻塞?
在 GMP(GNU 多精度算术库)调度过程中,可能会存在以下几种阻塞情况:
-
IO 阻塞:当 GMP 库进行 IO 操作时,如果 IO 操作需要等待数据读取或写入,此时 GMP 库的调度可能会被阻塞。
-
系统调用阻塞:当 GMP 库使用系统调用时,如申请内存、获取时间等,如果系统调用需要等待结果返回,此时 GMP 库的调度可能会被阻塞。
-
锁竞争阻塞:当多个线程同时访问 GMP 库的同一个数据结构时,可能会出现锁竞争的情况,如果某个线程获得锁并持有锁的时间过长,其他线程的调度可能会被阻塞。
-
垃圾回收阻塞:在 GMP 库中,存在一种称为 “垃圾回收” 的机制,用于释放不再使用的内存。当垃圾回收机制启动时,所有线程的调度都会被暂停,直到垃圾回收完成。
总之,GMP 调度过程中的阻塞情况可能会导致程序执行时间延长,因此在编写 GMP 应用程序时需要考虑如何避免或减少阻塞情况的发生。