Golang 协程Goroutine到底是怎么回事?(二)

共 2453字,需浏览 5分钟

 ·

2020-07-11 12:16

 协程不仅只有调度,还需要配套的齐全设备,比如协程锁,定时器,条件变量等



上一篇从协程的通用原理讲起,讲了通Golang的协程,使用一个完成的协程,必须要配合完善的配套设备,协程锁,定时器等,这文章就是描述于此。




Go 协程配套设备



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
  1. 当加锁失败,则保存上下文,把自己赋值到一个sudog结构里

  2. 挂接到锁内部相关队列里(semaRoot),root.queue() 。

  3. 调用goparkunlock主动切走,切到调度协程

sync.Mutex.Unlock-> runtime_Semrelease    -> sync_runtime_Semrelease        -> semrelease1
  1. 解锁

  2. 取出这个锁内部等待队列的一个元素(g)

  3. 调用goready唤醒goroutine,投入队列中,等待执行 


dc47a68da5b0d03400b6ce6ae0b0acad.webp


现在就以A, B任务同时处理Z来举例:

  1. A因为要修改Z,所以加了协程锁

  2. 加锁之后,由于处理一些其他的逻辑,因为某些等待事件,又把cpu切到M.g0调度了 (yield);注意了还没有放锁

  3. 这个时候M把B拿过来执行,yield to B

  4. B也要修改Z,这个时候发现锁已经被加上了,于是把自己挂到锁结构里面去

  5. 然后B直接切走,yield to M.g0

  6. 现在A的事件满足了,M.g0 重新调度到A执行,yield to A

  7. A 从刚刚切走的地方开始执行,然后放锁

    1. 注意了,放锁这里就会把B这个协程任务从锁队列中摘除,加到调度队列中,

  8. A执行完成之后,M.g0 调度B执行

  9. B从刚刚加锁的地方唤醒,于是加上锁了。然后走锁内逻辑,走完就放锁


以上就是协程锁的实现原理。保证A,B在修改Z的时候必须串行化。(旁白:加锁其实就是入队,串行入队,解锁就是出队,串行出队唤醒)


timer


time的实现原理:

  1. time.Sleep()的时候先创建好timer结构体,挂到哈希表

  2. 确保创建了一个goroutine(timeproc),这个会不断检查超时的timer

  3. 调用gopark保存栈,切到调度

  4. timeproc循环检查,当发现有超时的timer的时候,调用goready,把这个挂到运行队列里,等待运行


系统调用


对于某些系统调用,可能是会导致阻塞的,所以这个也必须封装才能让goroutine有让出cpu的机会。go内部实现系统调用会在前后包装两个函数:

entersyscallexitsyscall

解决syscall可能导致的问题关键就在这两个函数。这两个函数主要做了这些事情

entersyscall

  1. 设置p的状态为 _Psyscall

  2. 暂时解除P->M的绑定。但是M是有路径找到P的。并且虽然解除了P->M的绑定,但是这里并不会把P绑定到其他的M

exitsyscall

  1. 先尝试绑定到之前P

  2. 如果之前的P已经被sysmon处理掉了,那么则挑选一个空闲的P

  3. 如果还不行,则挂到全局队列sched里面去


(旁白:封装这两个函数,就是为了监控,不能让这一个系统调用阻塞了队列里所有的任务。你不能执行P了,就让给别人,就是这个思路)


sysmon线程就是处理_Psyscall状态的P,发现有超时的,则把P找个空闲的绑定,去执行P队列里的协程任务。 


G-M锁定

golang支持了一个G-M锁定的功能,通过lockOSThread和unlockOSThread来实现。主要是用于一些cgo调用,或者一些特殊的库,有些库是要求固定在一个线程上跑。


  1. G_a锁定M0 lockOSThread

  2. G_a调用gosched切走,投入P1队列

  3. M0调度,发现是lockedm,于是让出P0,自己调用notesleep睡眠

  4. M1取出G_a,发现是lockedg,于是让出P1给M0,并且唤醒M0. 自己变idle,stopm休眠

  5. 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的异步,配合协程一起使用。



a61b30224073083506dd35a36fcc0877.webp








推荐阅读





浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报