内容简介:过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索和总结。这是一个系列文章,本文为第五篇。就像前面几篇文章所描述的,开发者在日常开发中对并发的关注点主要是锁、管道(channel),比较少涉及到协程(goroutine) 的调度。不过了解协程的调度机制能够让开发者更好地认识并发的本质,从而在日常编码过程中做出更好的并
写在前面
过去 Web 开发的工作比较少涉及到并发的问题,每个用户请求在独立的线程里面进行,偶尔涉及到异步任务但是线程间数据同步模型非常简单,因此并未深入探究过并发这一块。最近在写游戏相关的服务端代码时发现数据的并发同步场景非常多,因此花了一点时间来探索和总结。这是一个系列文章,本文为第五篇。
就像前面几篇文章所描述的,开发者在日常开发中对并发的关注点主要是锁、管道(channel),比较少涉及到协程(goroutine) 的调度。不过了解协程的调度机制能够让开发者更好地认识并发的本质,从而在日常编码过程中做出更好的并发保证措施。本文简单介绍 Golang 中协程(goroutine)的调度及其抢占。
先看两段代码
第一段代码
下面的代码是《 浅谈 Golang 中数据的并发同步问题(二) 》中的一个示例 demo,为了说明问题增加了对单核的限制。运行 go run -race main.go
可以看到 Money: 1100
的输出。
下面的代码中包含两条 atomic.AddInt64
语句,分别在两个协程中运行。如果在 fmt.Printf
语句执行时子协程已经执行,输出结果是 Money: 2100
;当然,上面的代码极大概率会输出 Money: 1100
,即在 fmt.Printf
语句执行时子协程尚未执行。 那么, Golang 调度器何时才会调度并运行子协程呢?
// cat main.go package main import ( "fmt" "runtime" "sync/atomic" ) type Person struct { Money int64 } func main() { runtime.GOMAXPROCS(1) p := Person{Money: 100} go func() { atomic.AddInt64(&p.Money, 1000) }() atomic.AddInt64(&p.Money, 1000) fmt.Printf("Money: %d\n", atomic.LoadInt64(&p.Money)) }
第二段代码
我们可以构造一段与上一小节结构类似的代码,如下面的代码所示。通过 go run main.go
运行下面的代码可以看到输出结果中的 panic: hello goroutine
,却找不到 sum: xxxxxx
(如果看到的结果不一致,可以考虑增加 for
循环的终止判定条件)。 也就是说,下面的代码的子协程在代码退出前被成功调度。
// cat main.go package main import ( "fmt" "runtime" "time" ) func printTime(n int) { now := time.Now() fmt.Printf("Index: %d, Second: %d, NanoSecond: %d \n", n, now.Second(), now.Nanosecond()) } func main() { runtime.GOMAXPROCS(1) go func() { printTime(2) panic("hello goroutine") }() printTime(1) sum := 0 for i := 0; i < 666666666; i++ { sum += i } fmt.Printf("sum: %d\n", sum) }
协程 goroutine 的抢占
在 Golang 中可以非常方便地创建协程(goroutine),在可用核心一定的条件下,协程该如何有效地利用 CPU 资源呢?在《 Linux系统调度原理浅析(二) 》中简单描述过 Linux 内核的调度机制以及 goroutine 的调度机制:其中 Linux 内核通过时间片的方式给不同的系统线程分配 CPU 资源,Golang 则引入了 G/P/M 模型来实现调度,那么 Golang 的运行时(runtime)如何实现对 goroutine 的调度从而合理分配 CPU 资源呢?
G/P/M 模型
(图片摘自《 也谈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) 模型。具体地:
runtime.GOMAXPROCS(20)
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\n", sum)
的时候才被抢占(因为 for
循环里没有被插入抢占检查点,也就是说抢占检查点是编译器预先插入的,在非内联的函数的前面,具体可以查看最后几篇参考文章)。
小结
本文从两段代码切入,探究了 goroutine 的调度以及抢占。对于本文的第一段代码,主协程的代码并不需要消耗 forcePreemptNS
(默认为 10 ms)时长的资源,而主线程也没有主动把资源让出来,因此子协程没有运行。对于本文的第二段代码,由于 for
循环消耗了大量的计算资源,满足了 forcePreemptNS
时间阈值,调用 fmt.Printf
时触发了抢占,因此子协程得以运行。
本文的编写历时好几天,期间查阅了大量的资料,修修改改好几次,导致本文的架构比较松散;如果大家对本文有什么疑问,可以直接邮件我进行交流 。
参考
- skoo goroutine与调度器 比较经典的比喻,描述了调度的机制
- Golang goroutine - 简书 介绍了抢占式调度的机制
- 也谈goroutine调度器 - Tony Bai 比较系统地介绍了 goroutine 的调度
- Linux系统调度原理浅析(二) - 敬维 简单介绍了 linux 系统调度
- go/proc.go at release-branch.go1.12 · golang/go · GitHub
forcePreemptNS
强制协程调度的变量,默认为10 ms
。 - go/proc.go at release-branch.go1.12 · golang/go · GitHub 如果一个 goroutine 长时间占用计算资源,
sysmon
可以触发抢占;sysmon
在独立的 M(线程)中运行,且不需要绑定 P。 - go/stack.go at release-branch.go1.12 · golang/go · GitHub Golang 编译器会在每个非内联函数前面运行栈溢出检查,如果栈溢出则扩展栈,从而执行
newstack()
函数,继而触发抢占机制 - Debugging performance issues in Go programs - Intel® Software
- runtime: tight loops should be preemptible · Issue #10958 · golang/go · GitHub 介绍了空 for 的不可中断
- Scheduling In Go : Part II - Go Scheduler if you run any tight loops that are not making function calls, you will cause latencies within the scheduler and garbage collection
- goroutine - Why is this Go code blocking? - Stack Overflow 介绍了为什么 for{} 无限
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design Accessible Web Sites
Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95
It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!