分享
  1. 首页
  2. 文章

Golang协程调度二:协程切换原理

丁凯 · · 13030 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

概述

协程是Golang中的轻量级线程,麻雀虽小五脏俱全,Golang管理协程时也必然会涉及到协程之间的切换:阻塞的协程被切换出去,可运行的协程被切换进来。我们在本章节就来仔细分析下协程如何切换。

TLS

thread local storage:

getg()

goget()用来获取当前线程正在执行的协程g。该协程g被存储在TLS中。

mcall()

mcall在golang需要进行协程切换时被调用,用来保存被切换出去协程的信息,并在当前线程的g0协程堆栈上执行新的函数。一般情况下,会在新函数中执行一次schedule()来挑选新的协程来运行。接下来我们就看看mcall的实现。

调用时机

系统调用返回

当执行系统调用的线程从系统调用中返回后,有可能需要执行一次新的schedule,此时可能会调用mcall来完成该工作,如下:

func exitsyscall(dummy int32) {
 ......
 // Call the scheduler. 
 mcall(exitsyscall0)
 ......
}

在exitsyscall0中如果可能会放弃当前协程并执行一次schedule,挑选新的协程来占有m。

由于阻塞放弃执行

由于某些原因,当前执行的协程可能会被阻塞,如管道读写时条件无法满足,则当前协程会被阻塞直到条件满足。

在gopark()函数中,便会调用该mcall放弃当前协程并执行一次协程调度。

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
 mp := acquirem()
 gp := mp.curg 
 status := readgstatus(gp)
 if status != _Grunning && status != _Gscanrunning {
 throw("gopark: bad g status")
 }
 mp.waitlock = lock
 mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))
 gp.waitreason = reason
 mp.waittraceev = traceEv
 mp.waittraceskip = traceskip
 releasem(mp)
 // can't do anything that might move the G between Ms here. 
 mcall(park_m)
}

而park_m函数我们在后面会分析,它放弃之前执行的协程并调用一次schedule()挑选新的协程来执行。

执行原理

前面我们主要描述了mcall被调用的时机,现在我们要来看看mcall的实现原理。

mcall的函数原型是:

func mcall(fn func(*g))

这里fn的参数指的是在调用mcall之前正在运行的协程。

我们前面说到,mcall的主要作用是协程切换,它将当前正在执行的协程状态保存起来,然后在m->g0的堆栈上调用新的函数。 在新的函数内会将之前运行的协程放弃,然后调用一次schedule()来挑选新的协程运行。

// func mcall(fn func(*g)) 
// Switch to m->g0's stack, call fn(g). 
// Fn must never return. It should gogo(&g->sched) 
// to keep running g. 
TEXT runtime·mcall(SB), NOSPLIT, 0ドル-8
 // DI中存储参数fn 
 MOVQ fn+0(FP), DI 
 get_tls(CX)
 // 获取当前正在运行的协程g信息 
 // 将其状态保存在g.sched变量 
 MOVQ g(CX), AX // save state in g->sched 
 MOVQ 0(SP), BX // caller's PC 
 MOVQ BX, (g_sched+gobuf_pc)(AX)
 LEAQ fn+0(FP), BX // caller's SP 
 MOVQ BX, (g_sched+gobuf_sp)(AX)
 MOVQ AX, (g_sched+gobuf_g)(AX)
 MOVQ BP, (g_sched+gobuf_bp)(AX)
 // switch to m->g0 & its stack, call fn 
 MOVQ g(CX), BX
 MOVQ g_m(BX), BX
 MOVQ m_g0(BX), SI
 CMPQ SI, AX // if g == m->g0 call badmcall 
 JNE 3(PC)
 MOVQ $runtime·badmcall(SB), AX
 JMP AX
 MOVQ SI, g(CX) // g = m->g0 
 // 切换到m->g0堆栈 
 MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp 
 // 参数AX为之前运行的协程g 
 PUSHQ AX
 MOVQ DI, DX
 MOVQ 0(DI), DI 
 // 在m->g0堆栈上执行函数fn 
 CALL DI 
 POPQ AX
 MOVQ $runtime·badmcall2(SB), AX
 JMP AX
RET 

如何获取当前协程执行信息

前两句理解起来可能比较晦涩:

buf+0(FP) 其实就是获取gosave的第一个参数(gobuf地址),参考 A Quick Guide to Go’s Assembler

The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP).

LEAQ buf+0(FP), BX则是获取到第一个参数的存储地址,而根据golang的堆栈布局,这个地址其实是调用者的sp,如下:

接下来的几句比较容易理解,在第一句获取了gobuf的地址后,接下来将一些相关成员设置成合适的value。 最关键的是以下几句

get_tls(CX)
MOVQ g(CX), BX 
MOVQ BX, gobuf_g(AX)

这几句的作用是从TLS中获取当前线程运行的g,然后将其存储在gobuf的成员g。

gosave()

gosave在golang协程切换时被调用,用来保存被切换出去协程的信息,以便在下次该协程被重新调度执行时可以快速恢复出协程的执行上下文。

与协程调度相关的数据结构如下:

type g struct {
 stack stack 
 stackguard0 uintptr 
 stackguard1 uintptr 
 ......
 sched gobuf
 ......
}
// gobuf记录与协程切换相关信息 
type gobuf struct {
 sp uintptr 
 pc uintptr 
 g guintptr
 ctxt unsafe.Pointer 
 ret uintreg
 lr uintptr 
 bp uintptr 
}

gosave是用汇编语言写的,性能比较高,但理解起来就没那么容易。

TODO: gosave()的调用路径是什么呢?

// void gosave(Gobuf*)
// save state in Gobuf; setjmp 
TEXT runtime·gosave(SB), NOSPLIT, 0ドル-8 
 MOVQ buf+0(FP), AX // gobuf
 LEAQ buf+0(FP), BX // caller's SP
 MOVQ BX, gobuf_sp(AX)
 MOVQ 0(SP), BX // caller's PC
 MOVQ BX, gobuf_pc(AX)
 MOVQ 0,ドル gobuf_ret(AX)
 MOVQ 0,ドル gobuf_ctxt(AX)
 MOVQ BP, gobuf_bp(AX)
 get_tls(CX)
 MOVQ g(CX), BX 
 MOVQ BX, gobuf_g(AX)
RET 

前两句理解起来可能比较晦涩:

buf+0(FP) 其实就是获取gosave的第一个参数(gobuf地址),参考 A Quick Guide to Go’s Assembler

The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP).

LEAQ buf+0(FP), BX则是获取到第一个参数的存储地址,而根据golang的堆栈布局,这个地址其实是调用者的sp,如下:

接下来的几句比较容易理解,在第一句获取了gobuf的地址后,接下来将一些相关成员设置成合适的value。 最关键的是以下几句

get_tls(CX)
MOVQ g(CX), BX 
MOVQ BX, gobuf_g(AX)

这几句的作用是从TLS中获取当前线程运行的g,然后将其存储在gobuf的成员g。

gogo()

gogo的作用正好相反,用来从gobuf中恢复出协程执行状态并跳转到上一次指令处继续执行。因此,其代码也相对比较容易理解,我们就不过多赘述,如下:

gogo()主要的调用路径:schedule()–>execute()–>googo()

// void gogo(Gobuf*)
// restore state from Gobuf; longjmp 
TEXT runtime·gogo(SB), NOSPLIT, 0ドル-8 
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX 
MOVQ 0(DX), CX 
get_tls(CX)
MOVQ DX, g(CX)
MOVQ gobuf_sp(BX), SP // restore SP 
MOVQ gobuf_ret(BX), AX 
MOVQ gobuf_ctxt(BX), DX 
MOVQ gobuf_bp(BX), BP 
MOVQ 0,ドル gobuf_sp(BX)
MOVQ 0,ドル gobuf_ret(BX)
MOVQ 0,ドル gobuf_ctxt(BX)
MOVQ 0,ドル gobuf_bp(BX)
// 恢复出上一次执行指令,并跳转至该指令处
MOVQ gobuf_pc(BX), BX 
JMP BX 

这里最后一句跳转至该协程被调度出的那条语句继续执行,需要注意的是该函数不再返回调用者。


有疑问加站长微信联系(非本文作者)

本文来自:知乎专栏

感谢作者:丁凯

查看原文:Golang协程调度二:协程切换原理

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
13030 次点击 ∙ 1 赞
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏