Skip to content

线程调度

深入理解 Go 语言线程调度

  • 进程:资源分配的基本单位
  • 线程:调度的基本单位
  • 无论是线程还是进程,在 linux 中都以 task_struct 描述,从内核角度看,与进程无本质区别
  • Glibc 中的 pthread 库提供 NPTL(Native POSIX Threading Library)支持 线程调度

Linux 进程的内存使用

Linux进程的内存使用

➜  01syncmap git:(main) ✗ go build -o main main.go 
➜  01syncmap git:(main) ✗ ll 
总用量 1.2M
-rwxrwxr-x 1 cjx cjx 1.2M 9月  12 19:55 main
-rw-rw-r-- 1 cjx cjx  716 9月  12 17:38 main.go
➜  01syncmap git:(main) ✗ size main 
   text    data     bss     dec     hex filename
 785989   16432  212840 1015261   f7ddd main
➜  01syncmap git:(main) ✗ objdump -x main
......
000000000041c1c0 g     F .text  000000000000033e runtime.(*pallocData).findScavengeCandidate
000000000041c500 g     F .text  0000000000000125 runtime.(*stackScanState).putPtr
000000000041c640 g     F .text  000000000000013a runtime.(*stackScanState).getPtr
......
00000000004587c0 g     F .text  000000000000004c main.(*SafeMap).Write.dwrap.2
00000000004c21a0 g     O .noptrdata     0000000000000028 main..inittask
00000000004c2320 g     O .noptrdata     0000000000000038 sync..inittask
00000000004c6138 g     O .bss   0000000000000008 sync.expunged
00000000004c6390 g     O .bss   0000000000000018 sync.allPools
00000000004c63b0 g     O .bss   0000000000000018 sync.oldPools
......
00000000004c6068 g     O .bss   0000000000000008 _cgo_sigaction
000000000047c940 g     O .rodata        0000000000000008 runtime.mainPC
000000000047dbc8 g     O .rodata        0000000000000070 go.itab.*internal/reflectlite.rtype,internal/reflectlite.Type
000000000047ca00 g     O .rodata        000000000000000e runtime.defaultGOROOT.str
000000000047c970 g     O .rodata        0000000000000009 runtime.buildVersion.str
0000000000459000 g     O .rodata        0000000000000000 type.*
000000000047cb10 g     O .rodata        0000000000000018 runtime.textsectionmap

查看页大小

➜  01syncmap git:(main) ✗ getconf PAGESIZE
4096
➜  01syncmap git:(main) ✗ getconf PAGE_SIZE
4096

CPU 对内存的访问

  • CPU 上有个 Memory Management Unit(MMU) 单元
  • CPU 把虚拟地址给 MMU,MMU 去物理内存中查询页表,得到实际的物理地址
  • CPU 维护一份缓存 Translation Lookaside Buffer(TLB),缓存虚拟地址和物理地址的映射关系 CPU对内存的访问

进程切换开销

  • 直接开销
    • 切换页表全局目录(PGD)
    • 切换内核态堆栈
    • 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
    • 刷新 TLB
    • 系统调度器的代码执行
  • 间接开销
    • CPU 缓存失效导致的进程需要到内存直接访问的 IO 操作变多

线程切换开销

  • 线程本质上只是一批共享资源的进程,线程切换本质上依然需要内核进行进程切换
  • 一组线程因为共享内存资源,因此一个进程的所有线程共享虚拟地址空间,线程切换相比进程切换,主要节省了虚拟地址空间的切换

用户线程

无需内核帮助,应用程序在用户空间创建的可执行单元,创建销毁完全在用户态完成。 用户线程

Goroutine

Go 语言基于 GMP 模型实现用户态线程 - Goroutine:表示 goroutine,每个 goroutine 都有自己的栈空间,定时器,初始化的栈空间在 2k 左右,空间会随着需求增长。 - Machine:抽象化代表内核线程,记录内核线程栈信息,当goroutine 调度到线程时,使用该 goroutine 自己的栈信息。 - Process:代表调度器,负责调度 goroutine,维护一个本地goroutine 队列,M 从 P 上获得 goroutine 并执行,同时还负责部分内存的管理。 Goroutine

MPG 的对应关系

MPG的对应关系

GMP 模型细节

GMP模型细节

P 的状态

  • _Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
  • _Prunning :被线程 M 持有,并且正在执行用户代码或者调度器
  • _Psyscall:没有执行用户代码,当前线程陷入系统调用
  • _Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止
  • _Pdead :当前处理器已经不被使用 P的状态

G 的状态

  • _Gidle:刚刚被分配并且还没有被初始化,值为0,为创建 goroutine 后的默认值
  • _Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。
  • _Grunning: 正在执行代码的 goroutine,拥有栈的所有权
  • _Gsyscall:正在执行系统调用,拥有栈的所有权,与 P 脱离,但是与某个 M 绑定,会在调用结束后被分配到运行队列
  • _Gwaiting:被阻塞的 goroutine,阻塞在某个 channel 的发送或者接收队列
  • _Gdead: 当前 goroutine 未被使用,没有执行代码,可能有分配的栈,分布在空闲列表 gFree,可能是一个刚刚初始化的goroutine,也可能是执行了 goexit 退出的 goroutine
  • _Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在
  • _Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在

G的状态转换图

G的状态转换图

G 所处的位置

  • 进程都有一个全局的 G 队列
  • 每个 P 拥有自己的本地执行队列
  • 有不在运行队列中的 G
    • 处于 channel 阻塞态的 G 被放在 sudog
    • 脱离 P 绑定在 M 上的 G,如系统调用
    • 为了复用,执行结束进入 P 的 gFree 列表中的 G

Goroutine 创建过程

  • 获取或者创建新的 Goroutine 结构体
    • 从处理器的 gFree 列表中查找空闲的 Goroutine
    • 如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体
  • 将函数传入的参数移到 Goroutine 的栈上
  • 更新 Goroutine 调度相关的属性,更新状态为_Grunnable
  • 返回的 Goroutine 会存储到全局变量 allgs 中

将 Goroutine 放到运行队列上

  • Goroutine 设置到处理器的 runnext 作为下一个处理器执行的任务
  • 当处理器的本地运行队列已经没有剩余空间时(256),就会把本地队列中的一部分 Goroutine 和待加入的Goroutine 通过 runtime.runqputslow 添加到调度器持有的全局运行队列上

调度器行为

  • 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定几率(1/61)会从全局的运行队列中查找对应的 Goroutine
  • 从处理器本地的运行队列中查找待执行的 Goroutine
  • 如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找Goroutine
    • 从本地运行队列、全局运行队列中查找
    • 从网络轮询器中查找是否有 Goroutine 等待运行
    • 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取一半待运行的 Goroutine

课后练习 2.1

code

  • 将练习 1.2 中的生产者消费者模型修改成为多个生产者和多个消费者模式