greenlet是一个python coroutine库,可以提供比thread更加细粒度的并发控制。由于thread通常是一个内核对象,而coroutine通常只是工作在用户模式下,所以不存在线程切换时的开销。当使用coroutine和thread来实现并发时,将会得到更大的并发度。因为thread虽然比起process而言已经十分轻量,但是毕竟还是有开销的,当系统中有太多的thread在运行,也会拖累系统的运行速度,因为操作系统忙于执行context switch。
在python中,我们从thread中说获得并发度要比想象的低很多,因为thread其实不是完全并发执行的,这其中涉及到一个GIL(Global Interpreter Lock)的问题。关于GIL的问题,可以参考Understanding the Python GIL。
现在来简单讲解一下greenlet的实现:
switch to a new greenlet
stack overview
high +----------------+
^ | old data |
| +----------------+
| | g_switch |
| |----------------|
| | dummy marker |
| |----------------| <------- stack_stop _
| | other data | /
| +----------------+ |
| | g_initialstub | |
| +----------------+ |
| | g_switchstack | |
| +----------------+ /__ greenlet stack
| | slp_switch | /
| |----------------| |
| | stsizediff | |
| |----------------| |
| | stackref | |
| +----------------+ <------- stack_start _/
| | slp_save_state |
| +----------------+
| | g_save |
low +----------------+
在调用slp_save_state保存了target greenlet stack后, slp_switch会返回1, 此时控制流返回到g_initialstub.
此时会去调用PyEval_CallObjectWithKeywords, 调用greenlet的run函数, 从该函数返回后,将会切换到parent greenlet. 通常情况下,如果我们在创建greenlet时不指定parent,那么默认就是当前运行的greenlet: ts_current (green_new).
切换时,调用g_switch,并且将run的返回值传递给switch作为返回值。
当我们启动一个新的greenlet时,新的greenlet的stack必然在已有的greenlet之下。
running in a greenlet B, which is started by greenlet A, and try to switch to greenlet A.
stack overview
high +---------------+
^ | old data |
| +---------------+
| | g_switch |
| |---------------|
| | marker |
| +---------------+
| | g_initialstub |
| +---------------+
| | call run |
| +---------------+
| | ... ... |
| +---------------+
| | g_switch |
| |---------------|
| | marker |
| +---------------+
| | g_initialstub |
| +---------------+
| | call run |
| +---------------+
| | ... ... |
| +---------------+
| | g_switch |
| +---------------+
| | g_switchstack |
| +---------------+
| | slp_switch |
| |---------------|
| | stsizediff |
| |---------------|
| | stackref |
| +---------------+
| | slp_save_state|
| =================
low
在切换到一个已经运行的greenlet时,slp_save_state会save更多的东西。原来切换到一个新的greenlet,只需要save prev greenlet(通常是calling greenlet)的状态. 而现在,由于已经有多个calling greenlet运行,所以必须依次save这些greenlets.
当切换到一个greenlet A,A.stack_stop < ts_current.stack_start时,在restore A的stack后,会在当中产生一段空档。
比如A->B->C,C switch to A, 此时stack中只有A,之后A switch to C,此时C中save的stack_copy会与A的stack产生一个空挡,其中这个空档就是原来的B造成的。
return from a greenlet
在调用完slp_save_state后,由于切换到的是一个已经运行的greenlet,所以我们会接着调用
stsizediff = ts_target->stack_start - (char*)stackref
这里计算stack的差,因为在跳转到target greenlet后,我们target的stack必须是正确的。
还记得stackref吗,这个值是我们在调用到slp_switch时,esp的值,也就是说,我们每次slp_save_state的栈,其实是save到slp_switch为止的。
接着
mov eax, stsizediff add esp, eax add ebp, eax
这里将esp和ebp恢复到target greenlet调用slp_switch的状态。
之后,再调用slp_restore_state
memcpy(g->stack_start, g->stack_copy, g->stack_saved); PyMem_Free(g->stack_copy); g->stack_copy = NULL; g->stack_saved = 0;
这里的g是target greenlet, 这里将heap中的stack copy恢复到原来的位置上
restore后,heap中的stack被free
也就是说,只要是正在运行的greenlet(ts_current),必定没有stack_copy.
当从slp_switch返回后,我们设置了thread state,把frame指向目标的top_frame,这样就达到了恢复python中ip指针的目的。
这里很重要的一点是,slp_save_state 和 slp_restore_state在同一个函数里。因为我们save的stack的状态包括slp_switch的stack frame,当然restore之后,我们也就必须在这个函数中恢复执行才可以。
switch to self
greenlet.getcurrent可以获得当前的greenlet,这样我们不必显式传递greenlet。当switch to self时,其实是一个no-op。
调用g_switch之后,由于切换到自身,所以PyGreenlet_ACTIVE返回true, 我们进入g_switchstack, 随后进入到slp_switch,这里会接着调用slp_save_state。
while (ts_current->stack_stop < target_stop) { /* ts_current is entierely within the area to free */ if (g_save(ts_current, ts_current->stack_stop)) return -1; /* XXX */ ts_current = ts_current->stack_prev; } if (ts_current != ts_target) { if (g_save(ts_current, target_stop)) return -1; /* XXX */ }
由于ts_current = ts_target,所以不会进入while和if,于是state不会被save,这是正确的,因为当前正在运行的greenlet的stack_copy = NULL。
随后进入到slp_restore_state
if (ts_current->stack_stop == g->stack_stop) g->stack_prev = ts_current->stack_prev; else g->stack_prev = ts_current;
由于ts_current = g = ts_target,所以这里还是个no-op
接着返回到g_switchstack
tstate->frame = ts_target->top_frame;
这里恢复thread state的frame,还是一个no-op,最后返回到g_switch,函数返回switch参数,结束。