python中的generator(coroutine)淺析和應用

背景知識:python

  在Python中一個function要運行起來,它在python VM中須要三個東西。程序員

  1. PyCodeObject,這個保存了函數的代碼
  2. PyFunctionObject,這個表明一個虛擬機中的一個函數對象
  3. PyFrameObject,這個表明了函數運行時的調用鏈和堆棧

   Python正是經過這三樣東西模擬0x86的函數調用的網絡

 

  在python中 coroutine(協程)被稱爲的generator,這兩個東西在python實際上是同一個東東,之因此如此稱呼是由於它有迭代器的功能,可是又能夠只消耗不多的內存。不吃能存,又產生數據,稱爲generator仍是很符合情況的。數據結構

  Python中的generotor是一種PyFunctionCode 和PyFrameObject的包裝,這個生成器是有本身獨立 value stack 的。在加上它能在執行function code的中途返回,而且保存PyFrameObject的狀態。因此就有相似線程的一個主要做用了:可以被調度。app

  對於操做系統而言,它可以調度的只有線程,並且這種調度發生在內核態,調度時機對於程序員來講是不可知的。通常發生wait某個東西(鎖、網絡數據、磁盤數據)、時間片用完的時候,這個時候若是是非阻塞的返回,可是當前任務由於缺乏數據又不能繼續執行,做爲要榨乾CPU的程序員不能浪費掉分配到時間片,因此應該切換任務。若是一個線程表明一個任務的話,那麼在內核就多出一個線程對象。增長內存和調度程序的負擔,若是可以在用戶態有一種可以由程序員來控制調度的任務,便不用在內核態增長線程對象,任務調度由程序員負責。這個在用戶態能夠調度的東西就是coroutine了。由於能夠被切換,在一個線程內,它應該有本身的堆棧、本身寄存器(狀態)-------若是用C/C++這種語言實現的話,若是是在VM中實現,它在發生切換時,只要保持表明當前任務(其實就是函數)狀態的PyFrameObject的狀態就能夠了。函數


CPython generator涉及的數據結構和對象post

1.PyGen_Typespa

PyTypeObject PyGen_Type = {    PyVarObject_HEAD_INIT(&PyType_Type, 0)    
   "generator",                                /* tp_name */
    sizeof(PyGenObject),                        /* tp_basicsize */
    .........省略
    PyObject_GenericGetAttr,                    /* tp_getattro */
   ....... 省略
    (traverseproc)gen_traverse,                 /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    offsetof(PyGenObject, gi_weakreflist),      /* tp_weaklistoffset */
    PyObject_SelfIter,                          /* tp_iter */
    (iternextfunc)gen_iternext,                 /* tp_iternext */
    gen_methods,                                /* tp_methods */
    gen_memberlist,                             /* tp_members */
    gen_getsetlist,                             /* tp_getset */
    .......省略
    gen_del,                                    /* tp_del */
}; 

  從PyGen_Type這個對象對tp_iter,tp_iternext的設置來看,說明generator是實現了iterator protocol了,能夠在for 語句中迭代它。操作系統

 2.PyCodeObject、PyFrameObject,PyFunctionObject線程

 3.PyGenObject

typedef struct {
	PyObject_HEAD
	/* The gi_ prefix is intended to remind of generator-iterator. */
	/* Note: gi_frame can be NULL if the generator is "finished" */
	//PyFrameObject
	struct _frame *gi_frame;

	/* True if generator is being executed. */
	//狀態
	int gi_running;
	/* The code object backing the generator */
	//PyCodeObject
	PyObject *gi_code;
	/* List of weak reference. */
	PyObject *gi_weakreflist;
} PyGenObject;


PyGenObject中的gi_running表示狀態 0:沒有正在運行,1:正在運行,用frame.f_lasti==-1表示沒有啓動過,由於沒有運行過bytecode,因此frame的last instuction offset 會是-1,gi_code對應generator的方法代碼,gi_frame爲PyFrameObject,用於保存當前generator字節碼執行的狀態,能夠知道generator只能對應一個Frame,它不願有嵌套的Frame了,也就是不能在generator調用的函數中返回到send/next點,這個對與它的應用來講,會是一個限制,若是業務複雜會致使generator的代碼比較臃腫。

 

CPython 中generator的實現分析:

以這段python代碼爲分析對象

def gen():
	x=yield 1
	print x
	x=yield 2


g=gen()

g.next()
print g.send("sender")

  對應的Python bytecode爲

源碼行號 python代碼 字節碼偏移 字節碼 字節碼參數 註釋
1 def gen(): 0 LOAD_CONST

0 (<code object gen )

 這裏定義了一個PyFunctionObject,

對應的PyCodeObject

有一個flag(CO_GENERATOR)

標記是一個generator

    3 MAKE_FUNCTION 0  
    6 STORE_NAME 0(gen) gen=PyFunctionObject 
           
7 g=gen() 9 LOAD_NAME 0(gen)   
    12 CALL_FUNCTION  

 在PyEval_EvalCodeEX中,由於gen保存的

PyFunctionObject,

對應的PyCodeObject.co_flags

有CO_GENERATOR標記,

它直接返回返回一個PyGenObject

    15 STORE_NAME 1(g)  
           
 9  g.next() 18  LOAD_NAME  1(g)  
     21  LOAD_ATTR  2 (next)

PyObject_GetAttr(g,'next')

 PyGen_Type.tp_getattro()

此時tp_getattro=PyObject_GenericGetAttr

獲得wrappertype

這個wrapper包含了generator,

 

     24  CALL_FUNCTION  0  

在call 的時候,轉而調用 generator.next

就是gen_iternext,以後轉到

gen_send_ex這裏,

     27  POP_TOP    
           
 10    28  LOAD_NAME  1 (g)  
     31  LOAD_ATTR  3 (send)  
     34  LOAD_CONST  1 ('sender')  
     37  CALL_FUNCTION  1

 這裏轉到

gen_send(PyGenObject *gen,

    PyObject *arg)

     40  PRINT_ITEM    
     41  PRINT_NEWLINE    
     42  LOAD_CONST  2 (None)  
     45  RETURN_VALUE    
           

 

 在分析CPython源碼的時候會遇到許多的PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject、PyWrapperDescrObject,是由於Python語言設計的比較靈活,不一樣的方法、屬性,有不一樣的獲取方法,另外不一樣的方法有不一樣的參數,因此調用的方式也不同啊,因此對應的C代碼應該有不一樣的策略,須要包裝起到這個策略做用。這些Descr都是一些外層的包裝對象,只是爲了方便管理而已。在class object初始化的時候保存到相應的type.tp_dict中.

 

coroutine的應用: 

coroutine由於得不到操做系統的主動調用,要有程序員來控制調度時機,在用戶態的調度不適合模擬實時的狀體,可是很是適合作成無關時間的狀態改變,咱們以電商快遞商品過程的爲例,一個商品在賣家到達買家大體會經歷下面幾個狀態:待售、已售、商品在起始城市、商品在中間城市、商品到達目的城市、開始投遞、到達買家手中。

快遞商品狀態轉換圖

 電商商品狀態切換僞代碼:

from collections import namedtuple

State=namedtuple('State','statename action')

def commodity(id):
	#待售狀態
	action=yield State('forsale','online')

	#已售狀體
	if action=='sellout':
		action =yield State('sellout','postman1')
	elif action=='offline':
		return
	
	#在出發城市快遞點狀態
	if action=='store1':
		action=yield State('store1','store in garage')
	else:
		return 

	#已產生中間路徑狀態
	middleCities=generateRoute(id)
	if action=='route':
		action=yield State('store1_routed','caculate route')
	else:
		return

	l=len(middleCities)
	for city in middleCities:
		if action=='next':
			if city==middleCities[l-1]:
				#已經到達目的城市狀態
				action =yield State('destination',city)
			else:	
				#中間城市流轉狀態
				action=yield State('middle_city',city)

	#在目的城市開始投遞狀態
	if 'deliver':
		action=yield State('delivering','postman is delivering')
	else:
		return
	#被買家接受狀態
	if action=='accept':
		yield State('accepted','finish')
相關文章
相關標籤/搜索