概念
进程 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程至少有一个线程。进程切换只发生在内核态。
CPU调度
2kb8mb
并发 多线程程序在单核上运行
并行 多线程程序在多核上运行
协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度,所以golang中就会有调度器的存在。
详解
进程
CPUCPUCPUCPU
线程
有了多进程,为什么还要线程?原因如下:
- 进程间的信息难以共享数据,父子进程并未共享内存,需要通过进程间通信(IPC),在进程间进行信息交换,性能开销较大。
- 创建进程(一般是调用 fork 方法)的性能开销较大。
在一个进程内,可以设置多个执行单元,这个执行单元都运行在进程的上下文中,共享着同样的代码和全局数据,由于是在全局共享的,就不存在像进程间信息交换的性能损耗,所以性能和效率就更高了。这个运行在进程中的执行单元就是线程。
协程
官方的解释:
Goroutines
GogoroutinegoroutineCPUgoroutinegoroutines
从官方的解释中可以看到,协程是通过多路复用到一组线程上,所以本质上,协程就是轻量级的线程。但是必须要区分的一点是,协程是用用户态的,进程跟线程都是内核态,这点非常重要,这也是协程为什么高效的原因。
协程的优势如下:
CPUCPU64Linux8MB64MB执行Go协程只需要极少的栈内存,大概4~5KB,默认情况下,线程栈的大小为1MBIOIO
Golang GMP 调度器
简介
GgoroutineGogoMthreadGMPprocessorGMG
GoroutinePOSMM1OSCPU
线程和协程的映射关系
Golang
将独立执行的函数(协程)多路复用到一组线程上。 当协程阻塞时,例如通过调用阻塞系统调用,运行时会自动将同一操作系统线程上的其他协程移动到不同的可运行线程,这样它们就不会被阻塞。
也就是说,协程的执行是需要通过线程来先实现的。下图表示的映射关系:
在协程和线程的映射关系中,有以下三种:
N:11:1M:N
N:1
N111
缺点:
- 某个程序用不了硬件的多核加速能力
- 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。
1:1
11CPUN:1
缺点:
CPU
M:N
M1N:11:12CPUCPU
调度器实现原理
Go2012
2012
MGGMG
缺点:
GMMGGMGGGMGGMMCPU 在 M 之间的切换
2012 年之后的调度器实现原理,如下图所示:
M (thread) G (goroutine)P (Processor)ProcessorgoroutinegoroutinePPG
Gogoroutinegoroutine
Global QueueGPG256GGPGPPGOMAXPROCSMPPGPMGPPPMGGMPG
GoroutineOSMM1OSCPU
调度器设计策略
复用线程: 避免频繁的创建、销毁线程,而是对线程的复用。
work stealing
GPG
hand off
GPP
GOMAXPROCSPGOMAXPROCSCPUGOMAXPROCSGOMAXPROCS = 核数/2CPUcoroutineCPUGogoroutineCPUgoroutinegoroutinecoroutineGGMwork stealingPGGG
go func ()
流程如下:
go func ()goroutineGPGGPPGMMPMP1:1MPGPMPGMGMGsyscallMGruntimeMPdetachPMGPPPMG
调度器的生命周期
M0G0
M0
M00Mruntime.m0heapM0GM0M
G0
G0MgoroutineG0GG0MG0G0G0M0G0
我们来跟踪一段代码:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
接下来我们来针对上面的代码对调度器里面的结构做一个分析,也会经历如上图所示的过程:
runtimem0goroutine g02m0GOMAXPROCSPPmainmain.mainruntime1mainruntime.mainruntime.mainmain.mainruntime.maingoroutinemain goroutinemain goroutinePm0m0PPGmain goroutineGMGMGGMGmain.mainruntime.mainDeferPanicruntime.exit
Goruntime.maingoroutineruntime.maingoroutineruntime.main