goroutine 笔记

本文深入探讨了Go语言中的Goroutine调度器,从Goroutine与线程的区别开始,详细阐述了G-M模型、G-P-M模型的演变,以及抢占式调度、NUMA调度模型和其他优化措施。此外,还介绍了如何通过GODEBUG环境变量查看调度器状态,帮助读者理解Go并发执行的内部机制。
摘要由CSDN通过智能技术生成

1. goroutine 笔记

1.1. goroutine 与操作系统中线程的区别

同样作为并发单位, 也许你会认为 goroutine 就是操作系统中的线程, 这种认识是不对的, 接下来我们就来看看他们有哪些区别

1.1.1. 灵活的栈空间容量

操作系统中, 每个线程在创建时, 操作系统都会给他分配一个固定的栈空间, 通常容量为 2MB

而 GoLang 中, goroutine 十分灵活, 用户可能会一个 goroutine 中做繁重的工作, 也可能同时创建十万个 goroutine, 此时, 固定的栈空间就显得有些呆板, GoLang 中, 每个 goroutine 占用的栈空间大小都是动态变化的, 他可以按需增加或缩小, 最大限制达 1GB

1.1.2. goroutine 的调度

OS 线程由操作系统内核调度, 随着硬件时钟中断触发内核调度器, 内核调度器暂停当前线程的执行, 保存寄存器等信息到内存, 从内存中调度下一个要执行的线程来继续, 整个过程就是一个完整的上下文切换, 这是一个性能极低的操作

与操作系统类似, GoLang 也拥有一个用于调度 goroutine 的调度器, 但 GoLang 调度器不是由硬件时钟定期触发的, 而是由特定的 GoLang 语言结构触发的, 整个调度过程不涉及用户态与内核态的切换, 所以性能消耗要比操作系统线程的切换低很多

这个 GoLang 调度器也被称为 m:n 调度器, m 指的是被调度的 goroutine 数量, n 则指的是实际使用的线程数

环境变量 GOMAXPROCS 就是用来控制上面的 n 参数的大小的, 默认为机器上 CPU 的个数

1.1.3. 线程标识

每个操作系统的线程都拥有一个唯一的标识, 但在使用中, 很多程序员将线程标识与业务耦合在一起, 从而造成了很多十分诡异的现象和问题, 这与鼓励简单编程的 GoLang 风格相左, 所以 GoLang 拒绝为每个 goroutine 提供他们独有的标识

1.2. Goroutine 调度器

在 2016 年再次拿下 TIBOE 年度编程语言称号, 这充分证明了 Go 语言这几年在全世界范围内的受欢迎程度。如果要对世界范围内的 gopher 发起一次 “你究竟喜欢 Go 的哪一点” 的调查, 我相信很多 Gopher 会提到: goroutine。

Goroutine 是 Go 语言原生支持并发的具体实现, 你的 Go 代码都无一例外地跑在 goroutine 中。你可以启动许多甚至成千上万的 goroutine, Go 的 runtime 负责对 goroutine 进行管理。所谓的管理就是 “调度”, 粗糙地说调度就是决定何时哪个 goroutine 将获得资源开始执行、哪个 goroutine 应该停止执行让出资源、哪个 goroutine 应该被唤醒恢复执行等。goroutine 的调度是 Go team care 的事情, 大多数 gopher 们无需关心。但个人觉得适当了解一下 Goroutine 的调度模型和原理, 对于编写出更好的 go 代码是大有裨益的。因此, 在这篇文章中, 我将和大家一起来探究一下 goroutine 调度器的演化以及模型 / 原理。

注意: 这里要写的并不是对 goroutine 调度器的源码分析, 国内的雨痕老师在其《Go 语言学习笔记》一书的下卷 “源码剖析” 中已经对 Go 1.5.1 的 scheduler 实现做了细致且高质量的源码分析了, 对 Go scheduler 的实现特别感兴趣的 gopher 可以移步到这本书中去 0。这里关于 goroutine scheduler 的介绍主要是参考了 Go team 有关 scheduler 的各种 design doc、国外 Gopher 发表的有关 scheduler 的资料, 当然雨痕老师的书也给我了很多的启示。

1.3. Goroutine 调度器

提到 “调度”, 我们首先想到的就是操作系统对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理 CPU 上去运行。传统的编程语言比如 C、C++ 等的并发实现实际上就是基于操作系统调度的, 即程序负责创建线程 (一般通过 pthread 等 lib 调用实现), 操作系统负责调度。这种传统支持并发的方式有诸多不足:

  • 复杂

    • 创建容易, 退出难: 做过 C/C++ Programming 的童鞋都知道, 创建一个 thread(比如利用 pthread) 虽然参数也不少, 但好歹可以接受。但一旦涉及到 thread 的退出, 就要考虑 thread 是 detached, 还是需要 parent thread 去 join? 是否需要在 thread 中设置 cancel point, 以保证 join 时能顺利退出?
    • 并发单元间通信困难, 易错: 多个 thread 之间的通信虽然有多种机制可选, 但用起来是相当复杂; 并且一旦涉及到 shared memory, 就会用到各种 lock, 死锁便成为家常便饭;
    • thread stack size 的设定: 是使用默认的, 还是设置的大一些, 或者小一些呢?
  • 难于 scaling

    • 一个 thread 的代价已经比进程小了很多了, 但我们依然不能大量创建 thread, 因为除了每个 thread 占用的资源不小之外, 操作系统调度切换 thread 的代价也不小;
    • 对于很多网络服务程序, 由于不能大量创建 thread, 就要在少量 thread 里做网络多路复用, 即: 使用 epoll/kqueue/IoCompletionPort 这套机制, 即便有 libevent/libev 这样的第三方库帮忙, 写起这样的程序也是很不易的, 存在大量 callback, 给程序员带来不小的心智负担。

为此, Go 采用了用户层轻量级 thread 或者说是类 coroutine 的概念来解决这些问题, Go 将之称为 “goroutine”。goroutine 占用的资源非常小 (Go 1.4 将每个 goroutine stack 的 size 默认设置为 2k), goroutine 调度的切换也不用陷入 (trap) 操作系统内核层完成, 代价很低。因此, 一个 Go 程序中可以创建成千上万个并发的 goroutine。所有的 Go 代码都在 goroutine 中执行, 哪怕是 go 的 runtime 也不例外。将这些 goroutines 按照一定算法放到 “CPU” 上执行的程序就称为 goroutine 调度器或 goroutine scheduler。

不过, 一个 Go 程序对于操作系统来说只是一个用户层程序, 对于操作系统而言, 它的眼中只有 thread, 它甚至不知道有什么叫 Goroutine 的东西的存在。goroutine 的调度全要靠 Go 自己完成, 实现 Go 程序内 goroutine 之间 “公平” 的竞争 “CPU” 资源, 这个任务就落到了 Go runtime 头上, 要知道在一个 Go 程序中, 除了用户代码, 剩下的就是 go runtime 了。

于是 Goroutine 的调度问题就演变为 go runtime 如何将程序内的众多 goroutine 按照一定算法调度到 “CPU” 资源上运行了。在操作系统层面, Thread 竞争的 “CPU” 资源是真实的物理 CPU, 但在 Go 程序层面, 各个 Goroutine 要竞争的 “CPU” 资源是什么呢? Go 程序是用户层程序, 它本身整体是运行在一个或多个操作系统线程上的, 因此 goroutine 们要竞争的所谓 “CPU” 资源就是操作系统线程。这样 Go scheduler 的任务就明确了: 将 goroutines 按照一定算法放到不同的操作系统线程中去执行。这种在语言层面自带调度器的, 我们称之为原生支持并发。

1.4. Go 调度器模型与演化过程

1.4.1. G-M 模型

2012 年 3 月 28 日, Go 1.0 正式发布。在这个版本中, Go team 实现了一个简单的调度器。在这个调度器中, 每个 goroutine 对应于 runtime 中的一个抽象结构: G, 而 os thread 作为 “物理 CPU” 的存在而被抽象为一个结构: M(machine)。这个结构虽然简单, 但是却存在着许多问题。前 Intel blackbelt 工程师、现 Google 工程师 Dmitry Vyukov 在其《Scalable Go Scheduler Design》一文中指出了 G-M 模型的一个重要不足: 限制了 Go 并发程序的伸缩性, 尤其是对那些有高吞吐或并行计算需求的服务程序。主要体现在如下几个方面:

  • 单一全局互斥锁 (Sched.Lock) 和集中状态存储的存在导致所有 goroutine 相关操作, 比如: 创建、重新调度等都要上锁;
  • goroutine 传递问题: M 经常在 M 之间传递 “可运行” 的 goroutine, 这导致调度延迟增大以及额外的性能损耗;
  • 每个 M 做内存缓存, 导致内存占用过高, 数据局部性较差;
  • 由于 syscall 调用而形成的剧烈的 worker thread 阻塞和解除阻塞, 导致额外的性能损耗。

1.4.2. G-P-M 模型

于是 Dmitry Vyukov 亲自操刀改进 Go scheduler, 在 Go 1.1 中实现了 G-P-M 调度模型和 work stealing 算法, 这个模型一直沿用至今:

[外链图片转存失败, 源站可能有防盗链机制, 建议将图片保存下来直接上传 (img-6YYpdO3r-1669616161725)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/golang/golang-goroutine-scheduler-model.png)]

有名人曾说过: “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”, 我觉得 Dmitry Vyukov 的 G-P-M 模型恰是这一理论的践行者。Dmitry Vyukov 通过向 G-M 模型中增加了一个 P, 实现了 Go scheduler 的 scalable。

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 x M)。

1.4.3. 抢占式调度

G-P-M 模型的实现算是 Go scheduler 的一大进步, 但 Scheduler 仍然有一个头疼的问题, 那就是不支持抢占式调度, 导致一旦某个 G 中出现死循环或永久循环的代码逻辑, 那么 G 将永久占用分配给它的 P 和 M, 位于同一个 P 中的其他 G 将得不到调度, 出现 “饿死” 的情况。更为严重的是, 当只有一个 P 时 (GOMAXPROCS=1) 时, 整个 Go 程序中的其他 G 都将 “饿死”。于是 Dmitry Vyukov 又提出了《Go Preemptive Scheduler Design》并在 Go 1.2 中实现了 “抢占式” 调度。

这个抢占式调度的原理则是在每个函数或方法的入口, 加上一段额外的代码, 让 runtime 有机会检查是否需要执行抢占调度。这种解决方案只能说局部解决了 “饿死” 问题, 对于没有函数调用, 纯算法循环计算的 G, scheduler 依然无法抢占。

1.4.4. NUMA 调度模型

从 Go 1.2 以后, Go 似乎将重点放在了对 GC 的低延迟的优化上了, 对 scheduler 的优化和改进似乎不那么热心了, 只是伴随着 GC 的改进而作了些小的改动。Dmitry Vyukov 在 2014 年 9 月提出了一个新的 proposal design doc: 《NUMA‐aware scheduler for Go》, 作为未来 Go scheduler 演进方向的一个提议, 不过至今似乎这个 proposal 也没有列入开发计划。

1.4.5. 其他优化

Go runtime 已经实现了 netpoller, 这使得即便 G 发起网络 I/O 操作也不会导致 M 被阻塞(仅阻塞 G), 从而不会导致大量 M 被创建出来。但是对于 regular file 的 I/O 操作一旦阻塞, 那么 M 将进入 sleep 状态, 等待 I/O 返回后被唤醒; 这种情况下 P 将与 sleep 的 M 分离, 再选择一个 idle 的 M。如果此时没有 idle 的 M, 则会新创建一个 M, 这就是为何大量 I/O 操作导致大量 Thread 被创建的原因。

Ian Lance Taylor 在 Go 1.9 dev 周期中增加了一个 Poller for os package 的功能, 这个功能可以像 netpoller 那样, 在 G 操作支持 pollable 的 fd 时, 仅阻塞 G, 而不阻塞 M。不过该功能依然不能对 regular file 有效, regular file 不是 pollable 的。不过, 对于 scheduler 而言, 这也算是一个进步了。

1.5. Go 调度器原理的进一步理解

1.5.1. G、P、M

关于 G、P、M 的定义, 大家可以参见 $GOROOT/src/runtime/runtime2.go 这个源文件。这三个 struct 都是大块儿头, 每个 struct 定义都包含十几个甚至二、三十个字段。像 scheduler 这样的核心代码向来很复杂, 考虑的因素也非常多, 代码 “耦合” 成一坨。不过从复杂的代码中, 我们依然可以看出来 G、P、M 的各自大致用途(当然雨痕老师的源码分析功不可没), 这里简要说明一下:

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

//src/runtime/runtime2.go
type g struct {
        stack      stack   // offset known to runtime/cgo
        sched     gobuf
        goid        int64
        gopc       uintptr // pc of go statement that created this goroutine
        startpc    uintptr // pc of goroutine function
        ... ...
}

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...

    mcache      *mcache
    racectx     uintptr

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr

    // Available G's (status == Gdead)
    gfree    *g
    gfreecnt int32

  ... ...
}

type m struct {
    g0      *g     // goroutine with scheduling stack
    mstartfn      func()
    curg          *g       // current running goroutine
 .... ..
}

1.5.2. G 被抢占调度

和操作系统按时间片调度线程不同, Go 并没有时间片的概念。如果某个 G 没有进行 system call 调用、没有进行 I/O 操作、没有阻塞在一个 channel 操作上, 那么 m 是如何让 G 停下来并调度下一个 runnable G 的呢? 答案是: G 是被抢占调度的。

前面说过, 除非极端的无限循环或死循环, 否则只要 G 调用函数, Go runtime 就有抢占 G 的机会。Go 程序启动时, runtime 会去启动一个名为 sysmon 的 m(一般称为监控线程), 该 m 无需绑定 p 即可运行, 该 m 在整个 Go 程序的运行过程中至关重要:

//$GOROOT/src/runtime/proc.go

// The main goroutine.
func main() {
     ... ...
    systemstack(func() {
        newm(sysmon, nil)
    })
    .... ...
}

// Always runs without a P, so write barriers are not allowed.
//
//go:nowritebarrierrec
func sysmon() {
    // If a heap span goes unused for 5 minutes after a garbage collection,
    // we hand it back to the operating system.
    scavengelimit := int64(5 * 60 * 1e9)
    ... ...

    if  .... {
        ... ...
        // retake P's blocked in syscalls
        // and preempt long running G's
        if retake(now) != 0 {
            idle = 0
        } else {
            idle++
        }
       ... ...
    }
}

sysmon 每 20us~10ms 启动一次, 按照《Go 语言学习笔记》中的总结, sysmon 主要完成如下工作:

  • 释放闲置超过 5 分钟的 span 物理内存;
  • 如果超过 2 分钟没有垃圾回收, 强制执行;
  • 将长时间未处理的 netpoll 结果添加到任务队列;
  • 向长时间运行的 G 任务发出抢占调度;
  • 收回因 syscall 长时间阻塞的 P;

我们看到 sysmon 将 “向长时间运行的 G 任务发出抢占调度”, 这个事情由 retake 实施:

// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
          ... ...
           // Preempt G if it's running for too long.
            t := int64(_p_.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
                continue
            }
            if pd.schedwhen+forcePreemptNS > now {
                continue
            }
            preemptone(_p_)
         ... ...
}

可以看出, 如果一个 G 任务运行 10ms, sysmon 就会认为其运行时间太久而发出抢占式调度的请求。一旦 G 的抢占标志位被设为 true, 那么待这个 G 下一次调用函数或方法时, runtime 便可以将 G 抢占, 并移出运行状态, 放入 P 的 local runq 中, 等待下一次被调度。

1.5.3. channel 阻塞或 network I/O 情况下的调度

如果 G 被阻塞在某个 channel 操作或 network I/O 操作上时, G 会被放置到某个 wait 队列中, 而 M 会尝试运行下一个 runnable 的 G; 如果此时没有 runnable 的 G 供 m 运行, 那么 m 将解绑 P, 并进入 sleep 状态。当 I/O available 或 channel 操作完成, 在 wait 队列中的 G 会被唤醒, 标记为 runnable, 放入到某 P 的队列中, 绑定一个 M 继续执行。

1.5.4. system call 阻塞情况下的调度

如果 G 被阻塞在某个 system call 操作上, 那么不光 G 会阻塞, 执行该 G 的 M 也会解绑 P(实质是被 sysmon 抢走了), 与 G 一起进入 sleep 状态。如果此时有 idle 的 M, 则 P 与其绑定继续执行其他 G; 如果没有 idle M, 但仍然有其他 G 要去执行, 那么就会创建一个新 M。

当阻塞在 syscall 上的 G 完成 syscall 调用后, G 会去尝试获取一个可用的 P, 如果没有可用的 P, 那么 G 会被标记为 runnable, 之前的那个 sleep 的 M 将再次进入 sleep。

1.6. 调度器状态的查看方法

Go 提供了调度器当前状态的查看方法: 使用 Go 运行时环境变量 GODEBUG。

$GODEBUG=schedtrace=1000 godoc -http=:6060
SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 1001ms: gomaxprocs=4 idleprocs=0 threads=9 spinningthreads=0 idlethreads=3 runqueue=2 [8 14 5 2]
SCHED 2006ms: gomaxprocs=4 idleprocs=0 threads=25 spinningthreads=0 idlethreads=19 runqueue=12 [0 0 4 0]
SCHED 3006ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=8 runqueue=2 [0 1 1 0]
SCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=12 [6 3 1 0]
SCHED 5010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=1 idlethreads=20 runqueue=17 [0 0 0 0]
SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]
... ...

GODEBUG 这个 Go 运行时环境变量很是强大, 通过给其传入不同的 key1=value1,key2=value2… 组合, Go 的 runtime 会输出不同的调试信息, 比如在这里我们给 GODEBUG 传入了 "schedtrace=1000″, 其含义就是每 1000ms, 打印输出一次 goroutine scheduler 的状态, 每次一行。每一行各字段含义如下:

以上面例子中最后一行为例:

SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10]

SCHED: 调试信息输出标志字符串, 代表本行是 goroutine scheduler 的输出; 
6016ms: 即从程序启动到输出这行日志的时间; 
gomaxprocs: P 的数量; 
idleprocs: 处于 idle 状态的 P 的数量; 通过 gomaxprocs 和 idleprocs 的差值, 我们就可知道执行 go 代码的 P 的数量; 
threads: os threads 的数量, 包含 scheduler 使用的 m 数量, 加上 runtime 自用的类似 sysmon 这样的 thread 的数量; 
spinningthreads: 处于自旋状态的 os thread 数量; 
idlethread: 处于 idle 状态的 os thread 的数量; 
runqueue=1:  go scheduler 全局队列中 G 的数量; 
[3 4 0 10]: 分别为 4 个 P 的 local queue 中的 G 的数量。

我们还可以输出每个 goroutine、m 和 p 的详细调度信息, 但对于 Go user 来说, 绝大多数时间这是不必要的:

$ GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060

SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0
  P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17
  M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1
  G1: status=8() m=0 lockedm=0
  G17: status=3() m=1 lockedm=1

SCHED 1002ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0

 P0: status=2 schedtick=2293 syscalltick=18928 m=-1 runqsize=12 gfreecnt=2
  P1: status=1 schedtick=2356 syscalltick=19060 m=11 runqsize=11 gfreecnt=0
  P2: status=2 schedtick=2482 syscalltick=18316 m=-1 runqsize=37 gfreecnt=1
  P3: status=2 schedtick=2816 syscalltick=18907 m=-1 runqsize=2 gfreecnt=4
  M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M11: p=1 curg=6160 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
 ... ...

SCHED 2002ms: gomaxprocs=4 idleprocs=0 threads=23 spinningthreads=0 idlethreads=5 runqueue=4 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=0 schedtick=2972 syscalltick=29458 m=-1 runqsize=0 gfreecnt=6
  P1: status=2 schedtick=2964 syscalltick=33464 m=-1 runqsize=0 gfreecnt=39
  P2: status=1 schedtick=3415 syscalltick=33283 m=18 runqsize=0 gfreecnt=12
  P3: status=2 schedtick=3736 syscalltick=33701 m=-1 runqsize=1 gfreecnt=6
  M22: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
  M21: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1
... ...

关于 go scheduler 调试信息输出的详细信息, 可以参考 Dmitry Vyukov 的大作: 《Debugging performance issues in Go programs》。这也应该是每个 gopher 必读的经典文章。当然更详尽的代码可参考 $GOROOT/src/runtime/proc.go 中的 schedtrace 函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云满笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值