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

✍个人博客: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=Xscheddetail=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 种状态:

状态含义
_Gidle0刚刚被分配,还没有进行初始化。
_Grunnable1已经在运行队列中,还没有执行用户代码。
_Grunning2不在运行队列里中,已经可以执行用户代码,此时已经分配了 M 和 P。
_Gsyscall3正在执行系统调用,此时分配了 M。
_Gwaiting4在运行时被阻止,没有执行用户代码,也不在运行队列中,此时它正在某处阻塞等待中。
_Gmoribund_unused5尚未使用,但是在 gdb 中进行了硬编码。
_Gdead6尚未使用,这个状态可能是刚退出或是刚被初始化,此时它并没有执行用户代码,有可能有也有可能没有分配堆栈。
_Genqueue_unused7尚未使用。
_Gcopystack8正在复制堆栈,并没有执行用户代码,也不在运行队列中。

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)
状态含义
_Pidle0刚刚被分配,还没有进行进行初始化。
_Prunning1当 M 与 P 绑定调用 acquirep 时,P 的状态会改变为 _Prunning。
_Psyscall2正在执行系统调用。
_Pgcstop3暂停运行,此时系统正在进行 GC,直至 GC 结束后才会转变到下一个状态阶段。
_Pdead4废弃,不再使用。

42. GMP 调度过程中存在哪些阻塞?

在 GMP(GNU 多精度算术库)调度过程中,可能会存在以下几种阻塞情况:

  1. IO 阻塞:当 GMP 库进行 IO 操作时,如果 IO 操作需要等待数据读取或写入,此时 GMP 库的调度可能会被阻塞。

  2. 系统调用阻塞:当 GMP 库使用系统调用时,如申请内存、获取时间等,如果系统调用需要等待结果返回,此时 GMP 库的调度可能会被阻塞。

  3. 锁竞争阻塞:当多个线程同时访问 GMP 库的同一个数据结构时,可能会出现锁竞争的情况,如果某个线程获得锁并持有锁的时间过长,其他线程的调度可能会被阻塞。

  4. 垃圾回收阻塞:在 GMP 库中,存在一种称为 “垃圾回收” 的机制,用于释放不再使用的内存。当垃圾回收机制启动时,所有线程的调度都会被暂停,直到垃圾回收完成。

总之,GMP 调度过程中的阻塞情况可能会导致程序执行时间延长,因此在编写 GMP 应用程序时需要考虑如何避免或减少阻塞情况的发生。

  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值