Golang 协程Goroutine到底是怎么回事?(二)
“ 协程不仅只有调度,还需要配套的齐全设备,比如协程锁,定时器,条件变量等”
上一篇从协程的通用原理讲起,讲了通Golang的协程,使用一个完成的协程,必须要配合完善的配套设备,协程锁,定时器等,这篇文章就是描述于此。
Golang 协程锁,定时器,是怎么回事?系统调用又有什么特殊,G-M锁定是什么?
协程锁
之前提到,协程使用之后,是必须配套实现一些配件的。关键就是要保证在执行goroutine的时候不阻塞。最典型的的就是锁、timer、系统调用这三个方面。其中锁必须要是协程锁。
举例:某个场景,任务A需要修改Z,任务B也需要修改Z。如果是串行系统,A执行完了,再执行B,那么不会有问题。A -> B 。现在A,B是goroutine,可以并发执行,那么在操作Z的时候我们必须要有保证串行化的机制。
CO_LOCK
{
#处理逻辑
}
CO_UNLOCK
现在的关键点就是,我们不能直接用之前的mutex锁,或者是自旋锁。这样会严重影响并发,或者导致死锁。而必须配套实现协程锁。
sync.Mutex.Lock
-> runtime_SemacquireMutex
-> sync_runtime_SemacquireMutex
-> semacquire1 // runtime/sema.go
当加锁失败,则保存上下文,把自己赋值到一个sudog结构里
挂接到锁内部相关队列里(semaRoot),root.queue() 。
调用goparkunlock主动切走,切到调度协程
sync.Mutex.Unlock
-> runtime_Semrelease
-> sync_runtime_Semrelease
-> semrelease1
解锁
取出这个锁内部等待队列的一个元素(g)
调用goready唤醒goroutine,投入队列中,等待执行
现在就以A, B任务同时处理Z来举例:
A因为要修改Z,所以加了协程锁
加锁之后,由于处理一些其他的逻辑,因为某些等待事件,又把cpu切到M.g0调度了 (yield);注意了还没有放锁
这个时候M把B拿过来执行,yield to B
B也要修改Z,这个时候发现锁已经被加上了,于是把自己挂到锁结构里面去
然后B直接切走,yield to M.g0
现在A的事件满足了,M.g0 重新调度到A执行,yield to A
A 从刚刚切走的地方开始执行,然后放锁
注意了,放锁这里就会把B这个协程任务从锁队列中摘除,加到调度队列中,
A执行完成之后,M.g0 调度B执行
B从刚刚加锁的地方唤醒,于是加上锁了。然后走锁内逻辑,走完就放锁
以上就是协程锁的实现原理。保证A,B在修改Z的时候必须串行化。(旁白:加锁其实就是入队,串行入队,解锁就是出队,串行出队唤醒)
timer
time的实现原理:
time.Sleep()的时候先创建好timer结构体,挂到哈希表
确保创建了一个goroutine(timeproc),这个会不断检查超时的timer
调用gopark保存栈,切到调度
timeproc循环检查,当发现有超时的timer的时候,调用goready,把这个挂到运行队列里,等待运行
系统调用
对于某些系统调用,可能是会导致阻塞的,所以这个也必须封装才能让goroutine有让出cpu的机会。go内部实现系统调用会在前后包装两个函数:
entersyscall
exitsyscall
解决syscall可能导致的问题关键就在这两个函数。这两个函数主要做了这些事情
entersyscall
设置p的状态为 _Psyscall
暂时解除P->M的绑定。但是M是有路径找到P的。并且虽然解除了P->M的绑定,但是这里并不会把P绑定到其他的M
exitsyscall
先尝试绑定到之前P
如果之前的P已经被sysmon处理掉了,那么则挑选一个空闲的P
如果还不行,则挂到全局队列sched里面去
(旁白:封装这两个函数,就是为了监控,不能让这一个系统调用阻塞了队列里所有的任务。你不能执行P了,就让给别人,就是这个思路)
sysmon线程就是处理_Psyscall状态的P,发现有超时的,则把P找个空闲的绑定,去执行P队列里的协程任务。
G-M锁定
golang支持了一个G-M锁定的功能,通过lockOSThread和unlockOSThread来实现。主要是用于一些cgo调用,或者一些特殊的库,有些库是要求固定在一个线程上跑。
G_a锁定M0 lockOSThread
G_a调用gosched切走,投入P1队列
M0调度,发现是lockedm,于是让出P0,自己调用notesleep睡眠
M1取出G_a,发现是lockedg,于是让出P1给M0,并且唤醒M0. 自己变idle,stopm休眠
M0继续执行G_a
你可以发现,G_a只在M0上运行,锁定这段期间,M0也只执行了G_a任务。
当前go有哪些问题
当前go没有实现异步io。换句话说,如果在一个goroutine里面使用read/write io的系统调用,这些都是同步的io调用。会实实在在的阻塞M的调度,在遇到io延迟慢的时候,会导致sysmon检查到M-P超时(10ms),那么就会把M-P解绑,M游离出去执行阻塞任务,分配一个新的M来绑定P执行队列里的任务。
那么这种情况,虽然没有完全阻塞死P任务的执行,但是代价非常大,而且可能会导致M的数量一直飙升。就算没有这些极限情况,IO的并发能力相较于aio也是不行的。(旁白:Golang能切走的当前只有网络IO,磁盘io走的是系统调用,协程切不走)
当前net库是已经实现了底层的patch,aio还没有实现关键还是aio的复杂性导致的。 其实很多的工程实践是通过libaio来实现磁盘io的异步,配合协程一起使用。
推荐阅读