教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang-GMP调度详解

golang-GMP调度详解

发布时间:2022-02-25   编辑:jiaochengji.com
教程集为您提供golang-GMP调度详解等资源,欢迎您收藏本站,我们将为您提供最新的golang-GMP调度详解资源
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"/></svg><h1>传统并发</h1>

cpu切换浪费成本

<h1>
线程状态</h1>

线程可以有三中状态

等待中(waiting)

<ul><li>这意味着线程停止并等待某件事情以继续,这可能是因为等待硬件(磁盘、网络)、操作系统(系统调用)或异步调用(原子、互斥)等原因,这些类型的延迟是性能下降的根本原因</li></ul>

待执行(Runnable)

<ul><li>这意味着线程需要内核上的时间,以便执行它指定的机器指令。如果有很多线程都需要时间,那么线程需要等待更长的时间才能获得执行。此外,由于更多的线程在竞争,每个线程获得的单个执行时间都会缩短。这种类型的调度延迟也可能导致性能下降。</li></ul>

执行中(Executing)

<ul><li>这意味着线程已经被放置在一个核心上,并且正在执行它的机器指令。与应用程序相关的工作正在完成。这是每个人都想要的。</li></ul><h1>
线程工作状态</h1>

线程可以做两种类型的工作。第一个称为 CPU-Bound,第二个称为 IO-Bound

CPU-Bound这种工作类型永远也不会让线程处在等待状态,因为这是一项不断进行计算的工作。比如计算 π 的第 n 位,就是一个 CPU-Bound 线程。

IO-Bound这是导致线程进入等待状态的工作类型。比如通过网络请求对资源的访问或对操作系统进行系统调用。

<h1>
go的协程</h1>

<h1>
go 早期的调度模型</h1>

go协程放入全局队列,M获取和放回要枷锁和解锁

<ol><li>创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争</li><li>M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M’执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M’。</li><li>系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。</li></ol><h1>
异步网络调用</h1>

当系统执行异步的网路调用的时候,会使用网络轮询器的东西(netpoll)来更有效地处理系统调用,此时,会将G交给网络轮训器来执行

<h1>
同步系统调用</h1>

M会和P解绑定,M阻塞等G的执行,P在下一个G启动的时候寻找新的M的绑定执行,当此M执行完毕会优先寻找之前绑定的P,如果此时P不处于空闲状态,M查找其他的P,如果没有空闲的P。M会将G放入全局队列,等带执行

调度器介入后:识别出**<code>G1</code>已导致<code>M1</code>阻塞,此时,调度器将<code>M1</code><code>P</code>分离,同时也将<code>G1</code>带走。然后调度器引入新的<code>M2</code>来服务<code>P</code>。此时,可以从 LRQ 中选择<code>G2</code>并在<code>M2</code>**上进行上下文切换。

阻塞的系统调用完成后:**<code>G1</code>可以移回 LRQ 并再次由<code>P</code>**执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用。

<h1>
抢占调度</h1>

在<code>runtime.main</code>中会创建一个额外m运行<code>sysmon</code>函数,抢占就是在sysmon中实现的。

sysmon会进入一个无限循环, <mark>第一轮回休眠20us, 之后每次休眠时间倍增, 最终每一轮都会休眠10ms.</mark> sysmon中有netpool(获取fd事件), retake(抢占), forcegc(按时间强制执行gc), scavenge heap(释放自由列表中多余的项减少内存占用)等处理。

<h2>
2、抢占条件:</h2> <ol><li>如果 P 在系统调用中,且时长已经过一次 sysmon 后,则抢占;</li></ol>

调用 <code>handoffp</code> 解除 M 和 P 的关联。

<ol><li>如果 P 在运行,且时长经过一次 sysmon 后,并且时长超过设置的阻塞时长,则抢占;</li></ol>

设置标识,标识该函数可以被中止,当调用栈识别到这个标识时,就知道这是抢占触发的, 这时会再检查一遍是否要抢占。

<h1>
抢占思想</h1> <ol><li>sysmon中定期扫描正在执行的g列表,筛选出执行时间过长的g并且设置需要被抢占的标签.</li><li>在恰当的地方检测被抢占标记,(runtime主动)切换,让出cpu.</li></ol><h2>1、创建时间</h2>

sysmon不和任何的P绑定,是单独运行,负责G的监控及抢占

sysmon函数是Go runtime启动时创建的,负责监控所有goroutine的状态,判断是否需要GC,进行netpoll等操作。sysmon函数中会调用retake函数进行抢占式调度。

<h2>
2、抢占周期</h2>

关于扫描周期,至少是<code>20us</code>一个循环,后面视<code>idle</code>循环次数来进行<code>指数退避</code>(超过1ms之后倍增),但最长时间不超过<code>10ms</code>,故系统至多在<code>10ms</code>左右进行一次抢占检测.

也就是说当sysmon检测到M被阻塞了10ms,就会解绑M和P,然后别的M抢占P进行执行

<h2>
3、 怎么检测的</h2>

有计数来记录P的调度次数,还会记录上次执行的时间,如果下次检测P调度次数没有增加,则将当前时间更新,然后将P和M绑定,将当前的G和M绑定执行

<ol><li>通过遍历allp列表来获取正在运行的g.</li><li>状态检测.</li></ol><pre><code> t := int64(_p_.schedtick) if int64(pd.schedtick) != t { //在周期内已经调度过,即当前p上运行的g改变过. pd.schedtick = uint32(t) pd.schedwhen = now //更新最近一次抢占检测的时间 continue } if pd.schedwhen forcePreemptNS > now { continue } preemptone(_p_) </code></pre>

从上面关键数据结构得知 p.schedtick 记录了这个P上总共调度次数(递增), 故<code>sysmon</code>通过比较最近一次记录的<code>schedtick</code> 即可判断在一个周期内是否发生过调度行为.

通过最近一次检测时间与当前时间比较来明确是否需要抢占标记<code>pd.schedwhen forcePreemptNS>now</code>

<code>forcePreemptNS</code>为<code>10ms</code> ,如果超过10ms没有调度,则需要抢占, PS:并不能保证一个G最多运行10ms.

最后通过<code>preemptone</code>来标记当前G需要被抢占

<h2>
3、抢占触发</h2>

<code>func preemptone</code> 注释已经说了(runtime的注释很好),抢占触发时机: 目标g进行函数调用中触发栈检测过程中进行.

<pre><code>func testfunc()(sum int){ var nums[100] int for _, num := range nums { sum = num } return } </code></pre> <h1>
GMP模型</h1> <ul><li>本地队列不超过256G,优先将创建的G放入本地队列</li><li>最多可以有GOMAXPROCS个P</li><li>M分配的线程数,最大1万,runtime/debug/SetMaxThread,动态控制,有空闲回收,不够创建</li><li/></ul>

<h1>
调度器的设计策略—待补充</h1> <h2>1、复用线程</h2>

避免频繁的创建、销毁线程,而是对线程的复用。

1)work stealing机制

<h2>
2、利用并行</h2> <h2>3、抢占</h2> <ul><li>每个G最多10ms,后台sysmon监控</li></ul>

<h2>
4、work stealing机制</h2> <ul><li>优先从别的队列获取,每次获取二分之1,没有的话从全局队列,从别的本地队列偷的话,</li></ul>

<h1>
go func过程</h1> <ul><li>创建goroutine,优先放入本地队列,如果本地队列满了,放入全局,如果本地队列满了,优先从其他队列偷取,</li><li>一定时间也会去全局队列获取,防止饿死</li><li/></ul>

<h1>
M0 和G0的初始化</h1> <h2>M0 & G0</h2> <ul><li>M0在全局runtine.m0中,不需要在heap上分配,最大的栈</li><li>M0是没有栈增长检测的,绑定的G0是没有栈增长检测的,其他协程执行的函数都是G0最后执行的</li><li>G0是线程唯一的,负责调度其他的协程,其他G1到G2,要经过G0</li><li>在<code>调度或者系统调用</code>时,会使用M切换到G0,来调度M0的G0,会放在全局空间</li><li></li><li></li></ul><h2>执行流程</h2>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qj0tMM0a-1608540238460)(https://img.kancloud.cn/b3/10/b31027eeb493fa86654b41d46f34a98b_439x872.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-898A88da-1608540238461)(GMP.assets/image-20201215204818497.png)]

<h2>
可视化查看调度过程 trace</h2> <pre><code>package main import ( "fmt" "log" "os" "runtime/trace" ) func main() { //创建文件 f, err := os.Create("trace.out") if err != nil { log.Fatal(err) } defer f.Close() //启动 err = trace.Start(f) fmt.Println("hello") trace.Stop() } </code></pre> <pre><code> go run -race test.go </code></pre>

打开文件

<pre><code>go tool trace trace.out 2020/12/15 20:53:44 Parsing trace... 2020/12/15 20:53:44 Splitting trace... 2020/12/15 20:53:44 Opening browser. Trace viewer is listening on http://127.0.0.1:54494 </code></pre>

<h2>
GODEBUG</h2> <pre><code>package main import ( "fmt" "time" ) func main() { for i := 0; i < 50; i { time.Sleep(1 * time.Second) fmt.Println("hello") } } </code></pre> <pre><code>go build -o test2 test.go </code></pre> <pre><code>GODEBUG=schedtrace=1000 ./test2 SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0] hello SCHED 1004ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello SCHED 2012ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello SCHED 3017ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello SCHED 4022ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello </code></pre>

SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=4 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
hello

gomaxprocs=8 最大线程数

idleprocs=5 空闲的线程数

threads=4 使用线程数

spinningthreads=1 自旋线程

idlethreads=0 空闲线程

runqueue=0 [1 0 0 0 0 0 0 0] 第一个0 全局队列 然后是每个本地队列G的数量

<h1>
GMP 场景分析</h1> <h2>1、 G1 创建G3</h2>

当G1创建G3,为了保持局部性,优先加入G1所在的本地队列

<h2>
2、 G1 执行完毕</h2>

M优先从自己的本地队列获取G2执行

<h2>
3、 G2开辟过多的G</h2>

假设只能存4个G,那么由G2创建的G也会加入到本地队列

将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推

假设只能存4个G,那么由G2创建的G也会加入到本地队列

假设只能存4个G,那么由G2创建的G也会加入到本地队列

将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推,保证优先度一样

<h2>
4、 本地队列满在创建G</h2>

将本地队列从1/2划分,将头部的打乱和新创建的加入到全局队列,尾部的往前推,保证优先度一样

<h2>
5、唤醒正在休眠的M</h2>

每创建一个G的时候,尝试唤醒休眠队列的的一个M(前提是休眠M队列有M),然后和空闲的P绑定,没有的话,重新回到休眠队列

此时绑定了M的P就是自旋线程,会从别处偷取G执行

<h2>
6、被唤醒的M2 从全局队列获取执行</h2>

唤醒的M2,从全局队列获取G执行

调用G0切换到G3,执行,此时就不是自旋线程了

全局队列获取的个数`

<pre><code>n = min(len(GQ)/GOMAXPROCS 1,len(GQ/2)) GQ 全局队列G </code></pre>

<h2>
8、 M2从M1 偷取后半部分执行</h2>

M2被唤醒之后是自旋线程,全局队列位空

此时从其他队列偷取一半,的后半部分来到自己的本地队列执行

<h2>
9、自旋线程的最大限制</h2>

GOMAXPROCESS控制P的数量

最大的自旋线程数为GOMAXPROCESS-不是自旋的线程数

其他线程放入休眠线程池中

<h2>
10、G发生系统调用/阻塞</h2>

当M2发生系统调用或者网络请求阻塞的时候,M2会和P2解绑

解绑后的P2会寻找是否有空闲的M,如果有,就和其绑定,没有放入空闲P队列中

<h2>
11、G从阻塞到不阻塞</h2>

当阻塞的G和M2不阻塞之后,M2必须有P才可以执行G,优先获取P2

此时P2和P5绑定,那么会从空闲的P队列中获取是否有空空闲的P

如果没有,那么G会和M2解绑,将G放入全局队列

<h2>
12、休眠队列的回收</h2>

如果休眠线程队列长期没有被唤醒,就会被GC回收

<h2>
借鉴</h2>

https://www.kancloud.cn/aceld/golang/195830

刘丹冰大佬 欢迎大家支持大佬

到此这篇关于“golang-GMP调度详解”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang 调度器 GMP 原理与调度全分析
golang面试题分析03_GMP调度器
Golang的协程调度器原理及GMP设计思想?
golang的调度模型--GMP
golang https 全局代理_Golang 调度器
golang-GMP调度详解
Golang调度器GMP原理与调度全分析
golang sdk后端怎么用_Golang资深后端工程师需要了解的知识点
GO 语言之 Goroutine 原理解析
图解Go的channel底层原理

[关闭]
~ ~