Go中的协程(一) | 青训营笔记

81 阅读3分钟

为什么要有协程

进程

  • 操作系统程序的最小单位
  • 进程用来占用内存空间

线程

  • 每个进程可以有多个线程
  • 线程使用系统分配给进程的内存, 线程之间共享内存
  • 线程用来占用CPU时间
  • 现成的调度需要由系统进行, 开销较大

线程的问题

  • 线程本身占用资源大
  • 线程的操作开销大
  • 线程切换开销大

协程

  • 协程就是将一段程序的运行状态打包, 可以在线程之间调度
  • 将生产流程打包, 使得流程不固定在生产线上
  • 线程并不取代线程, 协程也要放在线程上运行
  • 线程是协程的资源, 协程使用线程这个资源

协程的优势

  • 资源利用
  • 快速调度
  • 超高并发

总结

  • 进程用于分配内存空间
  • 线程用来分配CPU空间
  • 协程用来精细利用线程
  • 协程的本质是一段包含了运行状态的程序

协程的本质

协程在Go语言中的本质就是一个g结构体(位于runtime2.go).

type g struct {
	stack stack // 栈信息
	// ...
	sched gobuf // 目前程序运行现场
	atomicstatus uint32 // 协程状态
	goid int64 // 协程的ID号  
}

type stack struct {
	lo uintptr
	hi uintptr
}

type gobuf struct {
	// stack pointer, 栈指针, 指向栈用到了什么地方
	sp uintptr
	// program counter, 程序计数器, 运行到了哪条代码
	pc uintptr
	// ...
}

Pasted image 20230520222112.png 协程如何描述(runtime2.go下的struct m)

type m struct {
	go *g // go语言第一个协程, 操作调度器
	curg *g // 正在运行的协程
	mOS // 针对操作系统所记录的线程信息(os_linux / windows / darwin ...)
}

协程如何执行

单线程循环(Go 0.x)

Pasted image 20230520222715.png

Pasted image 20230520223748.png

多线程循环(Go 1.0)

Pasted image 20230520223924.png

Pasted image 20230520224033.png

线程循环

  • 操作系统并不知道Goroutine的存在
  • 操作系统线程执行一个调度循环(Go代码或者会变组成),顺序执行Goroutine
  • 调度循环非常像线程池 问题:
  • 协程顺序执行, 无法并发
  • 多线程并发时, 会抢夺协程队列的全局锁

总结

  • 协程的本质是一个g结构体
  • 结构体记录了协程栈、PC信息
  • 最简情况下,线程执行标准调度循环,执行协程

G-M-P调度模型

前序的调度像是本地队列

type p struct {
	m muintptr // 指向相关的线程, 服务于一个线程
	// 可执行的协程队列, 可无锁访问
	runqhead uint32 // 队头
	runqtail uint32 // 队尾
	runq [256]guintptr
	runnext guintptr // 下一个可用的指针
}

Pasted image 20230520224937.png

Pasted image 20230520225028.png 如果本地协程执行完成, 获取全局的锁, 取全局拉取协程

Pasted image 20230520225134.png

P的作用

  • M与G之间的中介(送料器)
  • P持有一些G, 使得每次获取G的时候不用从全局找
  • 大大减少了并发冲突的情况

任务窃取

本地(m)和全局的携程队列都没有任务, 可以去别的p中获取, 增加了线程利用率

Pasted image 20230520225828.png

Pasted image 20230520225846.png

新建协程

  • 随机寻找一个P
  • 将新协程放入P的runnext(插队)
  • 如果P本地队列满, 放入全局队列

GMP模型减少了全局协程队列锁的获取