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

✍个人博客:Pandaconda-CSDN博客

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

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

34. Go  work stealing 机制?

概念

当线程 M ⽆可运⾏的 G 时,尝试从其他 M 绑定的 P 偷取 G,减少空转,提高了线程利用率(避免闲着不干活)。

当从本线程绑定 P 本地队列、全局 G 队列、netpoller 都找不到可执行的 g,会从别的 P 里窃取 G 并放到当前 P 上面。

从 netpoller 中拿到的 G 是 _Gwaiting 状态( 存放的是因为网络 IO 被阻塞的 G),从其它地方拿到的 G 是 _Grunnable 状态

从全局队列取的 G 数量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根据 GOMAXPROCS 负载均衡)

从其它 P 本地队列窃取的 G 数量:N = len(LRQ)/2(平分)

  

窃取流程

源码见 runtime/proc.go stealWork 函数,窃取流程如下,如果经过多次努力一直找不到需要运行的 goroutine 则调用 stopm 进入睡眠状态,等待被其它工作线程唤醒。

  1. 选择要窃取的 P

  2. 从 P 中偷走一半 G

选择要窃取的 P

窃取的实质就是遍历 allp 中的所有 p,查看其运行队列是否有 goroutine,如果有,则取其一半到当前工作线程的运行队列。

为了保证公平性,遍历 allp 时并不是固定的从 allp[0] 即第一个 p 开始,而是从随机位置上的 p 开始,而且遍历的顺序也随机化了,并不是现在访问了第 i 个 p 下一次就访问第 i + 1 个 p,而是使用了一种伪随机的方式遍历 allp 中的每个 p,防止每次遍历时使用同样的顺序访问 allp 中的元素。

offset := uint32(random()) % nprocs
coprime := 随机选取一个小于nprocs且与nprocs互质的数
const stealTries = 4 // 最多重试4次
for i := 0; i < stealTries; i++ {
    for i := 0; i < nprocs; i++ {
      p := allp[offset]
        从p的运行队列偷取goroutine
        if 偷取成功 {
        break
     }
        offset += coprime
        offset = offset % nprocs
     }
}

可以看到只要随机数不一样,偷取 p 的顺序也不一样,但可以保证经过 nprocs 次循环,每个 p 都会被访问到。

从 P 中偷走一半 G

源码见 runtime/proc.go runqsteal 函数:

挑选出盗取的对象 p 之后,则调用 runqsteal 盗取 p 的运行队列中的 goroutine,runqsteal 函数再调用 runqgrap 从 p 的本地队列尾部批量偷走一半的 g。

为啥是偷一半的 g,可以理解为负载均衡。

func runqgrab(_p_ *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {
    for {
        h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers
        t := atomic.LoadAcq(&_p_.runqtail) // load-acquire, synchronize with the producer
        n := t - h        //计算队列中有多少个goroutine
        n = n - n/2     //取队列中goroutine个数的一半
        if n == 0 {
            ......
            return ......
        }
        return n
    }
}

35. MP 中 w ork stealing 机制

GMP 中的 work stealing 机制是指在某个 M 线程的本地队列中没有 Goroutine 可供执行时,它会从其他 M 线程的本地队列中偷取 Goroutine 来执行。

具体地,work stealing 机制的实现过程如下:

  1. 每个 M 线程都有一个本地队列,用于存储待执行的 Goroutine。当一个 Goroutine 被创建时,它会被加入到一个本地队列中等待调度。

  2. 当一个 M 线程的本地队列中没有 Goroutine 可供执行时,它会从其他 M 线程的本地队列中随机选择一个队列,并尝试从该队列中偷取一些 Goroutine 来执行。在此期间,当前 M 线程会不断尝试从全局队列中获取 Goroutine 并将其调度到本地队列中执行。

  3. 当一个 M 线程偷取了其他 M 线程的 Goroutine 后,它会将这些 Goroutine 添加到自己的本地队列中,并将它们调度到绑定的 P 上执行。

work stealing 机制的好处是可以避免线程饥饿,提高 Goroutine 的调度效率。当某个 M 线程的本地队列中没有 Goroutine 可供执行时,它可以从其他 M 线程的队列中偷取 Goroutine 来执行,从而提高整个系统的并发能力和负载均衡性。同时,由于 work stealing 机制的实现比较复杂,因此在高并发场景下可能会增加一些额外的开销,需要谨慎使用。

实例

下面给出一个简单的示例代码,演示 GMP 中的 work stealing 机制:

package main
import (
    "fmt"
    "runtime"
    "sync"
    "time"
)
func main() {
    runtime.GOMAXPROCS(2) // 启用两个 P
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func() {
            time.Sleep(time.Second) // 模拟 Goroutine 的耗时操作
            fmt.Println("Goroutine executed.")
            wg.Done()
        }()
    }
    wg.Wait()
}

在上述代码中,我们启用了两个 P,并创建了 10 个 Goroutine。每个 Goroutine 都会执行一个耗时操作(这里用 time.Sleep() 来模拟),然后输出一条信息。

当这些 Goroutine 执行完毕后,我们可以观察到它们是如何在不同的 P 上执行的。由于 work stealing 机制的存在,每个 P 可能会从其他 P 的队列中偷取 Goroutine 来执行,从而达到负载均衡的目的。

36. Go hand off 机制?

概念

也称为 P 分离机制,当本线程 M 因为 G 进行的系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的 M 执行,也提高了线程利用率(避免站着茅坑不拉 shi)。

分离流程

当前线程 M 阻塞时,释放 P,给其它空闲的 M 处理。

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值