go中的Goroutine
概念
Goroutine可以理解为一种Go语言的协程(轻量级线程),是Go支持高并发的基础,属于用户态的线程,由Go runtime管理而不是操作系统
创建
通过go关键字调用底层函数runtime.newproc()创建一个goroutine
当调用该函数之后,goroutine会被设置成runnable状态
func main() {
go func() {
fmt.Println("func routine")
}()
fmt.Println("main goroutine")
}
创建好的这个goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址与程序计数器这些信息。
每个 G 在被创建之后,都会被优先放入到本地队列中,如果本地队列已经满了,就会被放入到全局队列中。
线程和goroutine的区别
Go goroutine泄露的场景
泄露原因
- Goroutine 内进行channel/mutex 等读写操作被一直阻塞。
- Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
- Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待
如何排查
单个函数:调用 runtime.NumGoroutine 方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了
生产/测试环境:使用PProf实时监测Goroutine的数量
用什么方法控制goroutine并发的数量
- 有缓冲的channel,利用缓冲满时发送阻塞的特性
- 无缓冲的channel,任务发送和执行分离,指定消费者并发协程数
go中的调度模式
go中早先的GM调度模式
GM调度存在的问题
- 全局队列的锁竞争,当 M 从全局队列中添加或者获取 G 的时候,都需要获取队列锁,导致激烈的锁竞争
- M 转移 G 增加额外开销,当 M1 在执行 G1 的时候, M1 创建了 G2,为了继续执行 G1,需要把 G2 保存到全局队列中,无法保证G2是被M1处理。因为 M1 原本就保存了 G2 的信息,所以 G2 最好是在 M1 上执行,这样的话也不需要转移G到全局队列和线程上下文切换
- 线程使用效率不能最大化,没有work-stealing 和hand-off 机制
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,为了解决这一的问题 go 从 1.1 版本引入P,在运行时系统的时候加入 P 对象,让 P 去管理这个 G 对象,M 想要运行 G,必须绑定 P,才能运行 P 所管理 的 G
GPM模式的设计策略
go中的GMP调度模式图:
上图中M开头的代表线程,P代表调度器,G代表协程,每一个调度器都有一个本地队列,里面存放的是待执行的G协程,而全局队列里面存放的也是待执行的G协程,但是只有所有的本地队列满了以后才会往全局队列中放G协程
复用线程
work stealing机制
还是以上图为例,假设有二个线程,M0 M1 M0下的调度器的本地队列有多个携程,而M1下的调度器的本地队列也有多个协程,但是M1下的协程执行完了,而M0下的协程还有很多,这个M1就会从M0下拿取一些协程去执行(先去全局队列中,如果没有全局队列再去M0),从而避免了资源浪费,这就work stealing机制
hand off机制
还是以上图为例,假设有二个线程,M0 M1 M0下的调度器的本地队列P1有多个携程,而M1下的调度器的本地队列P2也有多个协程,但是M0下正在执行的携程G1有大量的读取操作,导致M0堵塞,这样的话,调度器会再创建或者唤醒一个线程M3,将M1下P2的G2 G3等转移过去执行(就是把P1打包带走,除了G1),而G1与线程M0绑定到一起堵塞,堵塞完毕后G1重新回到其他本地队列中执行,而M1此时则睡眠或者销毁,这就是hand off机制
利用并行
GOMAXPROCS设置为cup/2
假设操作系统cpu为四个,那么个p的个数就为2个,其他的2个让给其他进程使用,提高并行
抢占
这个是与之前go的设计相比来说,以前的模式是一个cup与协程相绑定,如果协程不主动释放,那么就一直绑定,而现在一个cup与协程G1绑定,如果有等待的协程G2,则G1最多执行10ms就释放,G2就会抢占cup
全局G队列
还是以上图为例,假设有二个线程,M0 M1 M0下的调度器的本地队列有多个携程,而M1下的调度器的本地队列也有多个协程,M1先执行完后,然后去M0去偷,发现M0也没有了,那么就去全局G队列中去那协程,不过这个需要先对全局G队列中上锁,较慢