Go调度器介绍和容易忽视的问题

本文记录了本人对Golang调度器的理解和跟踪调度器的方法,特别是一个容易忽略的goroutine执行顺序问题,看了很多篇Golang调度器的文章都没提到这个点,分享出来一起学习,欢迎交流指正。

什么是调度器

为了方便刚接触操作系统和高级语言的同学,先用大白话介绍下什么是调度器。
调度,是将多个程序合理的安排到有限的CPU上来使得每个程序都能够得以执行,实现宏观的并发执行。比如我们的电脑CPU只有四核甚至双核,可是我们却可以在电脑上同时运行几十个程序,这就是操作系统调度器的功劳。但操作系统调度的是进程和线程,线程简单地说就是轻量级的进程,但是每个线程仍需要MB级别的内存,而且如果两个切换的线程在不同的进程中,还需要进程切换,会使CPU在调度这件事上花费大量时间。
为了更合理的利用CPU,Golang通过goroutine原生支持高并发,goroutine是由go调度器在语言层面进行调度,将goroutine安排到线程上,可以更充分地利用CPU。

Golang的调度器

Golang的调度器在runtime中实现,我们每个运行的程序执行前都会运行一个runtime负责调度goroutine,我们写的代码入口要在main包下的main函数中也是因为runtime.main函数会调用main.main。Golang的调度器在2012被重写过一次,现在使用的是新版的G-P-M调度器,但是我们还是先来看下老的G-M调度器,这样才可以更好的体会当前调度器的强大之处。

G-M模型:

下面是旧调度器的G-P模型:

M:代表线程,goroutine都是由线程来执行的;
Global G Queue:全局goroutine队列,其中G就代表goroutine,所有M都从这个队列中取出goroutine来执行。
这种模型比较简单,但是问题也很明显:

  1. 多个M访问一个公共的全局G队列,每次都需要加互斥锁保护,造成激烈的锁竞争和阻塞;
  2. 局部性很差,即如果M1上的G1创建了G2,需要将G2交给M2执行,但G1和G2是相关的,最好放在同一个M上执行。
  3. M中有mcache(内存分配状态),消耗大量内存和较差的局部性。
  4. 系统调用syscall会阻塞线程,浪费不能合理的利用CPU。

    G-P-M模型

    后来Go语言开发者改善了调度器为G-P-M模型,如下图:

其中G还是代表goroutine,M代表线程,全局队列依然存在;而新增加的P代表逻辑processor,现在G的眼中只有P,在G的眼里P就是它的CPU。并且给每个P新增加了局部队列来保存本P要处理的goroutine。
这个模型的调度方法如下:

  1. 每个P有个局部队列,局部队列保存待执行的goroutine
  2. 每个P和一个M绑定,M是真正的执行P中goroutine的实体
  3. 正常情况下,M从绑定的P中的局部队列获取G来执行
  4. 当M绑定的P的的局部队列已经满了之后就会把goroutine放到全局队列
  5. M是复用的,不需要反复销毁和创建,拥有work stealing和hand off策略保证线程的高效利用。
  6. 当M绑定的P的局部队列为空时,M会从其他P的局部队列中偷取G来执行,即work stealing;当其他P偷取不到G时,M会从全局队列获取到本地队列来执行G。
  7. 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand off,并寻找新的idle的M,若没有idle的M就会新建一个M。
  8. 当G因channel或者network I/O阻塞时,不会阻塞M,M会寻找其他runnable的G;当阻塞的G恢复后会重新进入runnable进入P队列等待执行
  9. mcache(内存分配状态)位于P,所以G可以跨M调度,不再存在跨M调度局部性差的问题
  10. G是抢占调度。不像操作系统按时间片调度线程那样,Go调度器没有时间片概念,G因阻塞和被抢占而暂停,并且G只能在函数调用时有可能被抢占,极端情况下如果G一直做死循环就会霸占一个P和M,Go调度器也无能为力。

Go调度器奇怪的执行顺序

是不是感觉自己对Go调度器工作原理已经有个初步的了解了?下面指出一个坑给你踩一下,小心了!
请看下面这段代码输出什么:

func main() {

    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        fmt.Println("--->", v)
        go func(u string) {
            fmt.Println(u)
            done <- true
        }(v)
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }

}

先仔细想一下再看答案哦!

实际的数据结果是:

---> a
---> b
---> c
c
b
a

Go调度器示例代码可以在跟着示例代码学golang中查看,持续更新中,想系统学习Golang的同学可以关注一下。

可能你的第一反应是“不应该是输出a,b,c,吗?为什么输出是c,a,b呢?”
这里我们虽然是使用for循环创建了3个goroutine,而且创建顺序是a,b,c,按之前的分析应该是将a,b,c三个goroutine依次放进P的局部队列,然后按照顺序依次执行a,b,c所在的goroutine,为什么每次都是先执行c所在的goroutine呢?这是因为同一逻辑处理器中三个任务被创建后 理论上会按顺序 被放在同一个任务队列,但实际上最后那个任务会被放在专一的next(下一个要被执行的任务的意思)的位置,所以优先级最高,最可能先被执行,所以表现为在同一个goroutine中创建的多个任务中最后创建那个任务最可能先被执行

这段解释来自参考文章《Goroutine执行顺序讨论》中。

# 调度器状态的查看方法

GODEBUG这个Go运行时环境变量很是强大,通过给其传入不同的key1=value1,key2=value2… 组合,Go的runtime会输出不同的调试信息,比如在这里我们给GODEBUG传入了”schedtrace=1000″,其含义就是每1000ms,打印输出一次goroutine scheduler的状态。
下面演示使用Golang强大的GODEBUG环境变量可以查看当前程序中Go调度器的状态:

环境为Windows10的Linux子系统(WSL),WSL搭建和使用的代码在learn-golang项目有整理,代码在文末参考的鸟窝的文章中也可以找到。

 func main() {
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go work(&wg)
    }
    wg.Wait()
    // Wait to see the global run queue deplete.
    time.Sleep(3 * time.Second)
}
func work(wg *sync.WaitGroup) {

    time.Sleep(time.Second)
    var counter int
    for i := 0; i < 1e10; i++ {
        counter++
    }
    wg.Done()
}

编译指令:

go build 01_GODEBUG-schedtrace.go
GODEBUG=schedtrace=1000 ./01_GODEBUG-schedtrace

结果:

SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [4 0 4 0]
SCHED 1000ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 2007ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 3025ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 4033ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 5048ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 6079ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 7081ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 8092ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 6]
SCHED 9113ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 10129ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 11134ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 12157ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 13170ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 14183ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 15187ms: gomaxprocs=4 idleprocs=0 threads=8 spinningthreads=0 idlethreads=3 runqueue=0 [0 1 0 1]
SCHED 16187ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 17190ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 18193ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 19196ms: gomaxprocs=4 idleprocs=2 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 [0 0 0 0]
SCHED 20200ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 21210ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]
SCHED 22219ms: gomaxprocs=4 idleprocs=4 threads=8 spinningthreads=0 idlethreads=6 runqueue=0 [0 0 0 0]

看到怎么多输出不要慌, 了解每个字段的含义就很清晰了:

  • SCHED 1000ms
    自程序运行开始经历的时间
  • gomaxprocs=4
    当前程序使用的逻辑processor,即P,小于等于CPU的核数。
  • idleprocs=4
    空闲的线程数
  • threads=8
    当前程序的总线程数M,包括在执行G的和空闲的
  • spinningthreads=0
    处于自旋状态的线程,即M在绑定的P的局部队列和全局队列都没有G,M没有销毁而是在四处寻觅有没有可以steal的G,这样可以减少线程的大量创建。
  • idlethreads=3
    处于idle空闲状态的线程
  • runqueue=0
    全局队列中G的数目
  • [0 0 0 6]
    本地队列中的每个P的局部队列中G的数目,我的电脑是四核所有有四个P。

上面的输出信息已经足够我们了解我们的程序运行状况,要想看每个goroutine、m和p的详细调度信息,可以在GODEBUG时加入,scheddetail

 GODEBUG=schedtrace=1000,scheddetail=1 ./01_GODEBUG-schedtrace

结果如下:
SCHED 0ms: gomaxprocs=4 idleprocs=4 threads=7 spinningthreads=0 idlethreads=2 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=0 schedtick=7 syscalltick=1 m=-1 runqsize=0 gfreecnt=0 P1: status=0 schedtick=2 syscalltick=1 m=-1 runqsize=0 gfreecnt=0 P2: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0 P3: status=0 schedtick=1 syscalltick=1 m=-1 runqsize=0 gfreecnt=0 M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 M5: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 M4: p=-1 curg=33 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 M3: p=-1 curg=49 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 M2: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1 M0: p=-1 curg=14 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1 G1: status=4(semacquire) m=-1 lockedm=-1 G2: status=4(force gc (idle)) m=-1 lockedm=-1 G3: status=4(GC sweep wait) m=-1 lockedm=-1 G4: status=4(sleep) m=-1 lockedm=-1 G5: status=4(sleep) m=-1 lockedm=-1 G6: status=4(sleep) m=-1 lockedm=-1 G7: status=4(sleep) m=-1 lockedm=-1 G8: status=4(sleep) m=-1 lockedm=-1 G9: status=4(sleep) m=-1 lockedm=-1 G10: status=4(sleep) m=-1 lockedm=-1 G11: status=4(sleep) m=-1 lockedm=-1 G12: status=4(sleep) m=-1 lockedm=-1 G13: status=4(sleep) m=-1 lockedm=-1 G14: status=3() m=0 lockedm=-1 G33: status=3() m=4 lockedm=-1 G17: status=3() m=2 lockedm=-1 G49: status=3() m=3 lockedm=-1

代码可以在跟着示例代码学golang中查看,持续更新中,想系统学习Golang的同学可以关注一下。

参考资料:

大彬Go调度器系列

也谈goroutine调度器

鸟窝 Go调度器跟踪

Go调度器详解

Goroutine执行顺序讨论

原文地址:https://www.cnblogs.com/CodeWithTxT/p/11370215.html

时间: 2024-07-29 18:19:08

Go调度器介绍和容易忽视的问题的相关文章

从零开始入门 K8s | 调度器的调度流程和算法介绍

导读:Kubernetes 作为当下最流行的容器自动化运维平台,以声明式实现了灵活的容器编排,本文以 v1.16 版本为基础详细介绍了 K8s 的基本调度框架.流程,以及主要的过滤器.Score 算法实现等,并介绍了两种方式用于实现自定义调度能力. 调度流程 调度流程概览 Kubernetes 作为当下最主流的容器自动化运维平台,作为 K8s 的容器编排的核心组件 kube-scheduler 将是我今天介绍的主角,如下介绍的版本都是以 release-1.16 为基础,下图是 kube-sch

linux调度器源码研究 - 概述(一)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 调度器作为操作系统的核心部件,具有非常重要的意义,其随着linux内核的更新也不断进行着更新.本系列文章通过linux-3.18.3源码进行调度器的学习和分析,一步一步将linux现有的调度器原原本本的展现出来.此篇文章作为开篇,主要介绍调度器的原理及重要数据结构. 调度器介绍 随着时代的发展,linux也从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设

Kubernetes 第十七章 调度器

来自: https://www.jianshu.com/p/acb34a1d1b6e Kubenernetes 调度器介绍 Kubernetes 调度器介绍 kube-scheduler是 kubernetes 系统的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理.更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由.如果一门新的技术不能帮助企业节约成本.提供效率,我相信是很难推进的.

MySQL的事件调度器使用介绍

MySQL的事件调度器使用介绍 自MySQL5.1.0起,增加了一个非常有特色的功能–事件调度器(Event Scheduler),可以用做定时执行某些特定任务,可以看作基于时间的触发器. 一.开启 事件调度默认是关闭的,开启可执行 SET GLOBAL event_scheduler=1; SET GLOBAL event_scheduler=ON; 或者在my.ini文件中加上event_scheduler=1 或者在启动命令后加上"-event_scheduler=1" 可以通过

简单介绍,基于ldirectord的高可用lvs-dr调度器

演示设备:物理机win7,虚拟机centos 7 DR1:node1,172.18.11.111 DR2:node2,172.18.11.112 VIP:172.18.11.200 RS1:172.18.11.11 RS2:172.18.11.12 配置高可用集群: 在DR上操作 ]# yum -y install pacemaker ]# yum install ldirectord-3.9.6-0rc1.1.1.x86_64.rpm ]# systemctl enable ldirector

Yarn 调度器Scheduler详解

理想情况下,我们应用对Yarn资源的请求应该立刻得到满足,但现实情况资源往往是有限的,特别是在一个很繁忙的集群,一个应用资源的请求经常需要等待一段时间才能的到相应的资源.在Yarn中,负责给应用分配资源的就是Scheduler.其实调度本身就是一个难题,很难找到一个完美的策略可以解决所有的应用场景.为此,Yarn提供了多种调度器和可配置的策略供我们选择. 一.调度器的选择 在Yarn中有三种调度器可以选择:FIFO Scheduler ,Capacity Scheduler,FairS ched

Linux 调度器发展简述

引言 进程调度是操作系统的核心功能.调度器只是是调度过程中的一部分,进程调度是非常复杂的过程,需要多个系统协同工作完成.本文所关注的仅为调度器,它的主要工作是在所有 RUNNING 进程中选择最合适的一个.作为一个通用操作系统,Linux 调度器将进程分为三类: 交互式进程 此类进程有大量的人机交互,因此进程不断地处于睡眠状态,等待用户输入.典型的应用比如编辑器 vi.此类进程对系统响应时间要求比较高,否则用户会感觉系统反应迟缓. 批处理进程 此类进程不需要人机交互,在后台运行,需要占用大量的系

YARN的capacity调度器主要配置分析

yarn中一个基本的调度单元是队列. yarn的内置调度器: 1.FIFO先进先出,一个的简单调度器,适合低负载集群.2.Capacity调度器,给不同队列(即用户或用户组)分配一个预期最小容量,在每个队列内部用层次化的FIFO来调度多个应用程序.3.Fair公平调度器,针对不同的应用(也可以为用户或用户组),每个应用属于一个队列,主旨是让每个应用分配的资源大体相当.(当然可以设置权重),若是只有一个应用,那集群所有资源都是他的. 适用情况:共享大集群.队列之间有较大差别. capacity调度

Linux IO 调度器

Linux IO Scheduler(Linux IO 调度器) 每个块设备或者块设备的分区,都对应有自身的请求队列(request_queue),而每个请求队列都可以选择一个I/O调度器来协调所递交的request.I/O调度器的基本目的是将请求按照它们对应在块设备上的扇区号进行排列,以减少磁头的移动,提高效率.每个设备的请求队列里的请求将按顺序被响应.实际上,除了这个队列,每个调度器自身都维护有不同数量的队列,用来对递交上来的request进行处理,而排在队列最前面的request将适时被移