写在文章开头
go语言
通过GMP模型
实现协程并发,为了避免单协程持续持有线程导致线程队列中的其他协程饥饿问题,设计者提出了一个抢占式调度机制,本文会基于一个简单的代码示例对抢占式调度过程进行深入讲解剖析。
Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。
详解协程抢占式调度
函数调用间进行抢占式调度
假设我们现在有这样一个协程,它会进行函数嵌套调用,代码如下所示:
scss
func foo1() {
fmt.Println("foo1调用foo2")
foo2()
}
func foo2() {
fmt.Println("foo2调用foo3")
foo3()
}
func foo3() {
fmt.Println("foo3")
}
func main() {
//设置WaitGroup等待协程运行结束
var wg sync.WaitGroup
wg.Add(1)
//通过协程调用foo1
go func() {
defer wg.Done()
foo1()
}()
//等待协程运行结束
wg.Wait()
}
我们给出运行结果:
foo1调用foo2
foo2调用foo3
foo3
基于这段代码示例,我们通过这段指令获取plan9
汇编码:
go
go build -gcflags -S main.go
可以看到在foo1
插入runtime.morestack_noctxt
方法,该方法是用于检查当前协程是否有足够的堆栈空间以保证函数的正常调用,基于这一点,go
就会在进行这部检查时顺带检查协程的执行时长,一旦超过10ms
该方法就会将协程设置为标记可被抢占:
less
0x0061 00097 (F:\github\test\main.go:8) CALL runtime.morestack_noctxt(SB)
如下图,我们的调用的函数都会被插入一个morestack
通过这个标记判断当前协程执行耗时,一旦发现超过10ms则会直接通过抢占式调度的方法g0
协程直接调用schedule
方法获取另外的协程进行调用:
这一点我们可以在asm_amd64.s
看到morestack
的newstack
的代码,而newstack就是实现抢占式调度的核心:
scss
TEXT runtime·morestack(SB),NOSPLIT,$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVQ g(CX), BX
MOVQ g_m(BX), BX
MOVQ m_g0(BX), SI
CMPQ g(CX), SI
JNE 3(PC)
CALL runtime·badmorestackg0(SB)
CALL runtime·abort(SB)
//......
//函数调用前会调用newstack进行抢占式的检查
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // crash if newstack returns
RET
上述的newstack
方法在stack.go
中,如果当前协程可被抢占则会调用gopreempt_m
回到g0
调用schedule
方法从协程队列中拿到新的协程执行任务:
scss
func newstack() {
preempt := stackguard0 == stackPreempt
//如果preempt 为true,则直接当前协程被标记为抢占直接调用gopreempt_m让出线程执行权
if preempt {
if gp == thisg.m.g0 {
throw("runtime: preempt g0")
}
//......
// Act like goroutine called runtime.Gosched.
gopreempt_m(gp) // never return
}
}
基于系统调用发起信号的抢占式调度
假设我们的协程没有进行额外的函数调用,是否就意味着当前协程的线程不能被抢占呢?很明显不是这样:
- 网络传输过程中需要发送某些紧急消息希望通过已有连接迅速将消息通知给对端时,就会产生
SIGURG
信号,go
语言就会在收到此信号时触发抢占式调度。
- 进行
GC
工作时像目标线程发送信号由此实现抢占式调度。
对于第一点我们可以在signal_unix.go
的sighandler
方法得以印证,可以看到它会判断sig 是否为_SIGURG
若是则调用doSigPreempt
进行抢占式调度
scss
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {
//如果传入的信号为_SIGURG则调用doSigPreempt回到schedule实现抢占式调度
if sig == sigPreempt && debug.asyncpreemptoff == 0 && !delayedSignal {
// Might be a preemption signal.
doSigPreempt(gp, c)
}
//......
}
doSigPreempt
会通过调用asyncPreempt
最终执行到preempt.go
的asyncPreempt2
调用到和上文函数调用抢占式调度方法gopreempt_m
回到schedule
方法从而完成抢占式调度:
scss
func doSigPreempt(gp *g, ctxt *sigctxt) {
//......
if wantAsyncPreempt(gp) {
if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {
// 调用asyncPreempt内部会得到一个和上文函数调用时抢占式调度的方法gopreempt_m的调用从而回到schedule方法
ctxt.pushCall(abi.FuncPCABI0(asyncPreempt), newpc)
}
}
//......
}
小结
以上便是笔者关于go语言中协程抢占式调度的所有内容,希望对你有帮助。
我是 sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。
参考
TCP 带外数据(即紧急模式的发送和接受) :blog.csdn.net/liushengxi_...
Linux(程序设计):59---SIGHUP、SIGPIPE、SIGURG信号处理(附SIGURG信号处理普通数据与外带数据案例):blog.51cto.com/u_15346415/...
本文使用 markdown.com.cn 排版