python 协程原理_Python协程greenlet实现原理
greenlet是stackless
Python中剝離出來(lái)的一個(gè)項(xiàng)目,可以作為官方CPython的一個(gè)擴(kuò)展來(lái)使用,從而支持Python協(xié)程。gevent正是基于greenlet實(shí)現(xiàn)。
協(xié)程實(shí)現(xiàn)原理
實(shí)現(xiàn)協(xié)程主要是在協(xié)程切換時(shí),將協(xié)程當(dāng)前的執(zhí)行上下文保存到協(xié)程關(guān)聯(lián)的context中。在c/c++這種native程序中實(shí)現(xiàn)協(xié)程,需要將棧內(nèi)容和CPU各個(gè)寄存器的內(nèi)容保存起來(lái)。在Python這種VM中則有些不同。例如,在以下基于greenlet協(xié)程的python程序中:
1
2
3
4
5
6
7
8
9
10
11
12
13def foo():bar()def bar():a = 3 + 1gr2.switch()def func():passgr1 = greenlet(foo)gr2 = greenlet(func)gr1.switch()
在bar中g(shù)r2.switch切換到gr2時(shí),協(xié)程庫(kù)需要保存gr1協(xié)程的執(zhí)行上下文。這個(gè)上下文包括:
Python VM的stack
Python VM中解釋執(zhí)行的上下文
理解以上兩點(diǎn)非常重要,至于為什么呢?想象一下如何去實(shí)現(xiàn)一個(gè)Python
VM,去解釋執(zhí)行一段Python代碼。其實(shí)這在任何基于VM的語(yǔ)言中,原理都是一樣的(native程序可以把x86物理CPU也視作特殊的VM)。可以參考Python解釋器簡(jiǎn)介-深入主循環(huán)。主要包含兩方面內(nèi)容:
VM在執(zhí)行代碼時(shí),其自身調(diào)用棧通常都是遞歸的
VM在執(zhí)行代碼時(shí),通常會(huì)創(chuàng)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)來(lái)表示代碼執(zhí)行塊,例如通常會(huì)有個(gè)struct Frame來(lái)表示一個(gè)函數(shù)
在VM的實(shí)現(xiàn)中通常會(huì)有類似以下的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct Frame {unsigned char *codes; // 存放代碼指令size_t pc; // 當(dāng)前執(zhí)行的指令位置int *stack; // stack-based的VM會(huì)有一個(gè)棧用于存放指令操作數(shù)};void op_call(frame) {switch (OP_CODE()) {case OP_CALL:child_frame = new_frame()op_call(child_frame)...case OP_ADD:op_add(...)}}
對(duì)應(yīng)到前面的Python例子代碼,在某一時(shí)刻VM的call stack可能是這樣的:
1
2
3op_addop_callop_call
理解了以上內(nèi)容后,就可以推測(cè)出greenlet本質(zhì)上也是做了以上兩件事。
greenlet實(shí)現(xiàn)原理
greenlet庫(kù)中每一個(gè)協(xié)程稱為一個(gè)greenlet。greenlet都有一個(gè)棧空間,如下圖:
圖中未表達(dá)出來(lái)的,greenlet的棧空間地址可能是重疊的。對(duì)于活躍的(當(dāng)前正在運(yùn)行)的greenlet,其棧內(nèi)容必然在c程序棧頂。而不活躍的被切走的greenlet,其棧內(nèi)容會(huì)被copy到新分配的堆內(nèi)存中。greenlet的棧空間是動(dòng)態(tài)的,其起始地址是固定的,但棧頂?shù)刂凡还潭āR韵麓a展示一個(gè)greenlet的棧空間如何確定:
1
2
3
4579 if (!PyGreenlet_STARTED(target)) { // greenlet未啟動(dòng),是一個(gè)需要新創(chuàng)建的greenlet580 void* dummymarker; // 該局部變量的地址成為新的greenlet的棧底581 ts_target = target;582 err = g_initialstub(&dummymarker); // 創(chuàng)建該greenlet并運(yùn)行
以上greenlet->stack_stop確定了棧底,而棧頂則是動(dòng)態(tài)的,在切換到其他greenlet前,對(duì)當(dāng)前greenlet進(jìn)行上下文的保存時(shí),獲取當(dāng)前的RSP(程序?qū)嶋H運(yùn)行的棧頂?shù)刂?#xff09;:
1
2
3
4
5
6
7
8
9
10410 static int GREENLET_NOINLINE(slp_save_state)(char* stackref)411 {412 /* must free all the C stack up to target_stop */413 char* target_stop = ts_target->stack_stop;414 PyGreenlet* owner = ts_current;415 assert(owner->stack_saved == 0);416 if (owner->stack_start == NULL)417 owner = owner->stack_prev; /* not saved if dying */418 else419 owner->stack_start = stackref; // stack_start指向棧頂
stackref是通過(guò)匯編獲取當(dāng)前RSP寄存器的值:
1__asm__ ("movl %%esp, %0" : "=g" (stackref));
保存棧內(nèi)容到堆內(nèi)存參看g_save的實(shí)現(xiàn),沒(méi)什么特別的。除了保存棧內(nèi)容外,如上一節(jié)講的,還需要保存VM執(zhí)行函數(shù)所對(duì)應(yīng)的Frame對(duì)象,這個(gè)在g_switchstack中體現(xiàn):
1
2
3
4
5
6
7
8
9460 PyThreadState* tstate = PyThreadState_GET(); // 獲取當(dāng)前線程的VM執(zhí)行上下文461 current->recursion_depth = tstate->recursion_depth;462 current->top_frame = tstate->frame; // 保存當(dāng)前正在執(zhí)行的frame到當(dāng)前正在執(zhí)行的greenlet...473 slp_switch(); // 做棧切換...487 PyThreadState* tstate = PyThreadState_GET();488 tstate->recursion_depth = target->recursion_depth;489 tstate->frame = target->top_frame; // 切換回來(lái)
上面的代碼展示VM frame的切換。接下來(lái)看下最復(fù)雜的部分,當(dāng)切換到目標(biāo)greenlet時(shí),如何恢復(fù)目標(biāo)greenlet的執(zhí)行上下文,這里主要就是恢復(fù)目標(biāo)greenlet的棧空間。假設(shè)有如下greenlet應(yīng)用代碼:
1
2
3
4
5
6
7
8
9def test1():gr2.switch()def test2():print('test2')gr1 = greenlet(test1)gr2 = greenlet(test2)gr1.switch()
在gr1中切換到gr2時(shí),也就是gr2.switch,會(huì)發(fā)生什么事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// g_switch 實(shí)現(xiàn)574 if (PyGreenlet_ACTIVE(target)) {575 ts_target = target; // 找到目標(biāo)greenlet,也就是gr2576 err = g_switchstack(); // 開(kāi)始切換// g_switchstack 實(shí)現(xiàn)462 current->top_frame = tstate->frame;...473 err = slp_switch();// slp_switch 實(shí)現(xiàn),根據(jù)不同平臺(tái)實(shí)現(xiàn)方式不同,原理相同69 SLP_SAVE_STATE(stackref, stsizediff);// 這個(gè)很重要,強(qiáng)行將當(dāng)前的棧指針ESP/EBP (32位OS)通過(guò)加上一個(gè)與目標(biāo)greenlet棧地址的偏移,而回到了// 目標(biāo)greenlet的棧空間。可以在下文看到stsizediff的獲取實(shí)現(xiàn)70 __asm__ volatile (71 "addl %0, %%esp\n"72 "addl %0, %%ebp\n"73 :74 : "r" (stsizediff)75 );76 SLP_RESTORE_STATE();// SLP_SAVE_STATE 實(shí)現(xiàn)316 #define SLP_SAVE_STATE(stackref, stsizediff) \317 stackref += STACK_MAGIC; \318 if (slp_save_state((char*)stackref)) return -1; \319 if (!PyGreenlet_ACTIVE(ts_target)) return 1; \// 獲取目標(biāo)greenlet的棧空間與當(dāng)前棧地址的偏移,用于稍后設(shè)置當(dāng)前棧地址回目標(biāo)greenlet的棧地址320 stsizediff = ts_target->stack_start - (char*)stackref// slp_save_state 沒(méi)啥看的,前面也提過(guò)了,主要就是復(fù)制當(dāng)前greenlet棧內(nèi)容到堆內(nèi)存// SLP_RESTORE_STATE 也沒(méi)什么看的,主要就是把greenlet堆內(nèi)存復(fù)制回棧空間
以上,首先將ESP/EBP的值改回目標(biāo)greenlet當(dāng)初切換走時(shí)的ESP/EBP值,然后再把greenlet的棧空間內(nèi)存(存放于堆內(nèi)存中)全部復(fù)制回來(lái),就實(shí)現(xiàn)了greenlet棧的回切。尤其注意的是,這個(gè)棧中是保存了各種函數(shù)的return地址的,所以當(dāng)slp_switch返回時(shí),就完全恢復(fù)到了目標(biāo)greenlet當(dāng)初被切走時(shí)棧上的內(nèi)容,包括各種函數(shù)調(diào)用棧。而當(dāng)前greenlet的棧,則停留在了類似以下的函數(shù)調(diào)用棧:
1
2
3g_switchstackg_switch...
參考
總結(jié)
以上是生活随笔為你收集整理的python 协程原理_Python协程greenlet实现原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python自己创建模块_创建并发布自己
- 下一篇: bentley 二次开发_Bentley