goroutine学习笔记
进程、线程、协程
进程:系统进行资源分配的基本单位,由独立的内存空间
线程:CPU调度和分配资源的基本单位,线程依附于进程,每个线程会共享父进程的资源
协程:一种用户态的轻量级线程,协程完全由用户控制,协程间的切换只需要保存任务的上下文,没有内核开销
优势
- 内存消耗更小
- 一个线程可以包含多个协程
- 线程大约申请8MB内存(线程栈空间通常是 2M)
- 协程大约4kb(Goroutine 栈空间最小 2K)
- 上线文切换更快
- 协程少一道手续,协程只涉及到3个寄存器值的修改;而线程需要涉及到模式切换(用户态切换到内核态),以及16个寄存器、PC、SP...等寄存器的修改
- 线程申请内存,需要走内核;协程申请内存,不需要走内核
线程上下文切换,由于中断处理、多任务处理、用户态切换等原因CPU从一个线程切换到另一个线程,需要保存当前线程的状态并恢复到另一个线程的状态。上下文切换的代价是高昂的。上下文切换的延迟取决于不同的因素,大概在在 50 到 100 纳秒之间,考虑到硬件在内核上平均每纳秒大约执行12条指令,那么一次上下文切换花费600到1200条指令的执行时间。如果存在跨核上下文切换,可能导致CPU缓存失效(CPU从缓存访问数据的成本约3 - 40个时钟周期,从主存访问则是100到300个时钟周期),这中场景的切换成本更高
G-P-M模型
Go 调度器模型我们通常叫做G-P-M 模型,他包括 4 个重要结构,分别是G、P、M、Sched
G: Goroutine,每个Goroutine
对应一个 G 结构体,G 存储Goroutine的运行堆栈、状态以及任务函数,可重用。G并非执行体,每个G需要绑定到P才能被调度执行
P:Processor,表示逻辑处理器,对G来说,P相当于CPU核,G只有绑定P才能被调度执行。对于M来说,P执行了相应的执行环境(context),如内存分配状态(mcache)任务队列(G)等。P的数量决定了系统内最大可执行G的数量(前提CPU核数 >= P数量)。P的数量由用户设置的GOMAXPROCS
决定,最大为256。
M: Machine, OS内核线程抽象,代表真正执行计算的资源,在绑定有效P后,进入 schedule 循环;而P的循环机制大致是从gobal
队列、P的local
队列以及wait
队列获取。M的数量不是固定的,由go runtime调整。为了防止过度创建OS线程导致系统调度不过来,目前最大限制为10000个。M不保留G状态,这是G可以跨M调度的基础。
注意: 设置 GOMAXPROCS 高于真正可使用的核心数后会导致Go调度器不停地进行OS线程切换,从而给调度器增加很多不必要的工作。go会获取CPU核心数作为GOMAXPROCS,docker会隔离CPU,导致无法获取到正确的CPU核数,目前 Go 官方并无好的方式来规避在容器里获取不到真正可使用的核心数这一问题,而 Uber 提出了一种 Workaround
方法,利用 uber-go/automaxprocs 这一个包,可以在运行时根据 cgroup 为容器分配的CPU资源限制数来修改 GOMAXPROCS。
网友评论