golang goroutine实现_Golang 并发问题(五)goroutine 的调度及抢占

Go语言中文网,致力于每日分享编码知识,欢迎关注我,会有意想不到的收获!

75654a1c87ba0ec0e53efb3aa27c58cb.png

01 写在前面

过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索和总结。这是一个系列文章,本文为第五篇。

就像前面几篇文章所描述的,开发者在日常开发中对并发的关注点主要是锁、管道(channel),比较少涉及到协程(goroutine) 的调度。不过了解协程的调度机制能够让开发者更好地认识并发的本质,从而在日常编码过程中做出更好的并发保证措施。本文简单介绍 Golang 中协程(goroutine)的调度及其抢占。

02 先看两段代码

1162c74069327925bc5ada784e801611.png

第一段代码

下面的代码是浅谈 Golang 中数据的并发同步问题(二)中的一个示例 demo,为了说明问题增加了对单核的限制。运行 go run -race main.go 可以看到 Money: 1100 的输出。

下面的代码中包含两条 atomic.AddInt64 语句,分别在两个协程中运行。如果在 fmt.Printf 语句执行时子协程已经执行,输出结果是 Money: 2100;当然,上面的代码极大概率会输出 Money: 1100 ,即在 fmt.Printf 语句执行时子协程尚未执行。那么, Golang 调度器何时才会调度并运行子协程呢?

a45c6918de093189fe454c28a47e86f3.png

第二段代码

我们可以构造一段与上一小节结构类似的代码,如下面的代码所示。通过 go run main.go 运行下面的代码可以看到输出结果中的 panic: hello goroutine ,却找不到sum: xxxxxx(如果看到的结果不一致,可以考虑增加 for 循环的终止判定条件)。也就是说,下面的代码的子协程在代码退出前被成功调度。



5c9f301bb81604d12709b6416a1a100c.png

03 协程 goroutine 的抢占

在 Golang 中可以非常方便地创建协程(goroutine),在可用核心一定的条件下,协程该如何有效地利用 CPU 资源呢?在《Linux系统调度原理浅析(二)》中简单描述过 Linux 内核的调度机制以及 goroutine 的调度机制:其中 Linux 内核通过时间片的方式给不同的系统线程分配 CPU 资源,Golang 则引入了 G/P/M 模型来实现调度,那么 Golang 的运行时(runtime)如何实现对 goroutine 的调度从而合理分配 CPU 资源呢?

G/P/M 模型

6d256b5fb8ed7a1236faf5047ed6a908.png

(图片摘自《也谈goroutine调度器 - Tony Bai》)

P 是一个“逻辑 Proccessor”,每个 G 要想真正运行起来,首先需要被分配一个 P(进入到 P 的 local runq 中,这里暂忽略 global runq 那个环节)。对于 G 来说,P 就是运行它的 “CPU”,可以说:G 的眼里只有 P。但从 Go scheduler 视角来看,真正的 “CPU” 是 M,只有将 P 和 M 绑定才能让 P 的 runq 中 G 得以真实运行起来。这样的 P 与 M 的关系,就好比 Linux 操作系统调度层面用户线程 (user thread) 与核心线程 (kernel thread) 的对应关系那样,都是 (n × m) 模型。具体地:

  • G: 表示 goroutine,存储了 goroutine 的执行 stack 信息、goroutine 状态以及 goroutine 的任务函数等;另外 G 对象是可以重用的。
  • P: 表示逻辑 processor,P的数量决定了系统内最大可并行的 G 的数量(前提是系统的物理 CPU 核数>=P 的数量,可以 runtime.GOMAXPROCS(20)增加 P 数量大于 CPU 核数);P 的最大作用还是其拥有的各种 G 对象队列、链表、一些 cache 和状态。
  • M: M代表着真正的执行计算资源。在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行G的函数,调用 goexit 做清理工作并回到 M,如此反复。M并不保留 G 状态,这是 G 可以跨 M 调度的基础。

goroutine 发生调度的时机

假如忽略(G、P、M)模型的复杂性,可以想象一个 goroutine 获得计算资源(CPU)后一般不能一直运行到完毕,它们往往可能要等待其他资源才能执行完成,比如读取磁盘文件内容、通过 RPC 调用远程服务等,在等待的过程中 goroutine 是不需要消耗计算资源的,因此调度器可以把计算资源给其他的 goroutine 使用。

参考《Golang goroutine》可以知道,goroutine 遇到下面的情况下可能会产生重新调度(大家判断哪些代码属于下面这些情况):

  • 阻塞 I/O
  • select操作
  • 阻塞在channel
  • 等待锁
  • 主动调用 runtime.Gosched()

goroutine 的抢占

如果一个 goroutine 不包含上面提到的几种情况,那么其他的 goroutine 就无法被调度到相应的 CPU 上面运行,这是不应该发生的。这时候就需要抢占机制来打断长时间占用 CPU 资源的 goroutine ,发起重新调度。

Golang 运行时(runtime)中的系统监控线程 sysmon 可以找出“长时间占用”的 goroutine,从而“提醒”相应的 goroutine 该中断了。

特别说明-1:sysmon在独立的 M(线程)中运行,且不需要绑定 P。这意味着,runtime.GOMAXPROCS(1)限制 P 的数量为 1 的情况下,即使一个 goroutine 一直占用这个 P 进行密集型计算(意味着 goroutine 一直占有唯一的 P),依然不影响 sysmon 的正常运行。

特别说明-2:sysmon 可以找到“长时间占用 P”的 goroutine,但也只是标记 goroutine 应该被抢占了,并无法强制进行 goroutine 的切换。因此本文的 “第二段代码” 在进行 for 循环时并不会被抢占,而是在 for 循环结束后执行 fmt.Printf("sum: %d

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值