起源
對tornado的StackContext的研究起源于一個優化問題.后來研究討論的優化方案,需要修改每一個函數入參(OMG)或者只需協程安全的全局變量.
但是tornado的協程只是個抽象概念,沒有實體.比如線程要實現這個,有個threading local就可以(可以放置線程獨立的全局資源).如果協程也有類似的功能就完美了,所以一個StackContent出現了.這貨是什么出身,參考tornnado/ stack_context.py的第一段注釋:
`StackContext` allows applications to maintain threadlocal-like state that follows execution
as it moves to other execution contexts.
是的,沒錯,這貨就是用來維護協程的上下文, 以實現協程的'threadlocal'功能.
研究
StackContext的核心代碼就在tornnado/stack_context.py中. 下面主要分析其中的幾個核心部分.
1. 首先映入我們眼簾的是
class _State(threading.local):
def __init__(self):
self.contexts = (tuple(), None)
_state = _State()
暫時不用太關心實現細節(本來也沒多少細節好不).主要說明的是_state是線程獨立(因為繼承于threading.local).然后簡單的回顧下知識點:一個進程可以有多個線程,但是對于單核cpu,同一時間只能有一個線程在執行,當某一個線程執行時,寄存器的狀態,特有數據的狀態(threading.local)等等組成了他執行的上下文環境.線程不停切換時,上下文也在不停的切換.現在針對協程,我們做個映射, 把進程映射為線程,把線程映射為協程. _state就是用來指向當前執行協程的上下文環境.(應該還沒暈吧)
2.我們繼續拾級而上,看到的是:
class StackContext(object):
def __init__(self, context_factory):
self.context_factory = context_factory
self.contexts = []
self.active = True
def _deactivate(self):
self.active = False
# StackContext protocol
def enter(self):
context = self.context_factory()
self.contexts.append(context)
context.__enter__()
def exit(self, type, value, traceback):
context = self.contexts.pop()
context.__exit__(type, value, traceback)
def __enter__(self):
self.old_contexts = _state.contexts
self.new_contexts = (self.old_contexts[0] + (self,), self)
_state.contexts = self.new_contexts
try:
self.enter()
except:
_state.contexts = self.old_contexts
raise
return self._deactivate
def __exit__(self, type, value, traceback):
try:
self.exit(type, value, traceback)
finally:
final_contexts = _state.contexts
_state.contexts = self.old_contexts
if final_contexts is not self.new_contexts:
raise StackContextInconsistentError(
'stack_context inconsistency (may be caused by yield '
'within a "with StackContext" block)')
self.new_contexts = None
StackContext就是我所說的上下文對象了.但是是時候坦白了,這貨其實只是個跑腿的.真正管理協程上下文的是
def __init__(self, context_factory):
self.context_factory = context_factory
context_factory 可以理解為一個上下文的管理者,由它來生成一個真正的上下文context, context控制一個協程上下文的創建和退出.在協程切換時,StackContext會告訴前一個context你被開除了(__exit__
),并告訴context_factory趕緊給我找個新的context, 項目就要開工了(__enter__
).
于是下面這兩個函數就很好理解了:
def enter(self):
context = self.context_factory()
self.contexts.append(context)
context.__enter__()
就是先用context_factory上下文管理者生成一個上下文,然后保存該上下文(退出時用),最后進入了該上下文
def exit(self, type, value, traceback):
context = self.contexts.pop()
context.__exit__(type, value, traceback)
這就是先取出最后的一個上下文,然后退出.
這就是StackContext,context_factory,context三者的關系了.
上面說明了StackContext對于協程上下文的創建和摧毀,下面說明下StackContext:
def __enter__(self):
self.old_contexts = _state.contexts
self.new_contexts = (self.old_contexts[0] + (self,), self)
_state.contexts = self.new_contexts
try:
self.enter()
except:
_state.contexts = self.old_contexts
raise
return self._deactivate
StackContext因為是棧式上下文,所以__enter__
里面干的活就是:先保存現有的上下文,再將自己放入上下文堆棧的棧頂,最后重新設置當前的上下文環境.
3.最后高潮即將來臨:我們先總結下:
- _state用來指向當前運行協程的上下文的,協程不斷切換過程中,_state也指向不同的上下文.
- StackContext負責上下文切換的具體工作,即退出之前的上下文,進入新的上下文,忙成狗的角色.
有個這兩個對象,最后看下wrap函數(簡化后):
def wrap(fn):
cap_contexts = [_state.contexts]
def wrapped(*args, **kwargs):
ret = None
try:
current_state = _state.contexts
# Remove deactivated items
cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
# Force new state
_state.contexts = contexts
# Apply stack contexts
last_ctx = 0
stack = contexts[0]
# Apply state
for n in stack:
try:
n.enter()
last_ctx += 1
except:
pass
if top is None:
try:
ret = fn(*args, **kwargs)
except:
exc = sys.exc_info()
top = contexts[1]
# If there was exception, try to handle it by going through the exception chain
if top is not None:
exc = _handle_exception(top, exc)
else:
# Otherwise take shorter path and run stack contexts in reverse order
while last_ctx > 0:
last_ctx -= 1
c = stack[last_ctx]
try:
c.exit(*exc)
except:
exc = sys.exc_info()
top = c.old_contexts[1]
break
else:
top = None
# If if exception happened while unrolling, take longer exception handler path
if top is not None:
exc = _handle_exception(top, exc)
# If exception was not handled, raise it
if exc != (None, None, None):
raise_exc_info(exc)
finally:
_state.contexts = current_state
return ret
wrapped._wrapped = True
return wrapped
這里有兩種上下文:定義時上下文和執行時上下文.定義時上下文是協程函數定義時指定的上下文, 運行時上下文是協程函數運行時系統所處的上下文.即協程函數定義時說,我要在有空調,有可樂的環境下工作,但是系統在不停切換后,切換到那個協程時,系統環境只有個破風扇在轉著.
協程在這種情況下,只能自己創造自己喜歡的環境了(將運行時環境改造成定義說明的環境).當初研究到這我有點想不通,定義時的環境如何一直保存著呢?答案是通過閉包.
現在再看這個函數時,就比較好理解了, cap_contexts = [_state.contexts]
就是將定義時上下文保存到了cap_contexts
.而wrapped
就是我們最終扔給IoLoop
的協程函數了.wrapped
具體什么時候執行,執行時候的_state是什么,都是不確定的,所以wrapped
主要工作就是將cap_contexts
保存的上下文,替換到當前上下文中.
下面基本分析下流程:
cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
移除定義時有效,但是執行時已經無效的上下文.
for n in stack:
try:
n.enter()
last_ctx += 1
except:
pass
每個stack里面的元素的就是StackContext對象,也即按從棧底到棧頂的順序,逐一恢復到定義時上下文環境.
if top is None:
try:
ret = fn(*args, **kwargs)
如果恢復時沒有異常,才開始執行真正的協程代碼
while last_ctx > 0:
last_ctx -= 1
c = stack[last_ctx]
try:
c.exit(*exc)
協程函數執行結束后,按相反的順序退出棧式上下文.
基本上大致流程就是這樣了.
wrap
函數主要的使用場景就是將協程函數放入協程引擎前(IOLoop),加上一層上下文管理功能.具體可參加tornado/ioloop.py的PollIOLoop.add_callback
應用
以上扯了這么多,其實就是說明了tornado協程上下文切換的大體機制,但是具體的上下文還是需要自己實現,而實現的關鍵就是context_factory
.
github上就有人實現了一個context_factory 地址是:https://github.com/viewfinderco/viewfinder/blob/master/backend/base/context_local.py.
只要寫個子類繼承下里面的ContextLocal
, 你就擁有了一個context_factory
,按他文章的例子,定義一個子類MyContext, 那么就可以按如下代碼使用:
yield run_with_stack_context(StackContext(MyContext(coroutine_value)), your_func)
這里需要說明幾點:
1 coroutine就相當于協程獨立的變量,就是我們最終想要的功能,可以實現一個管理協程資源的類,然后將他的實例傳遞進去.
2 上面這個寫法只是針對your_func是協程函數的情況,如果針對普通函數,只需:
with StackContext(MyContext(coroutine_value)):
your_func()
3 為什么針對協程函數會這么特別,這是因為直接用普遍函數的調用方法會導致下上文堆棧不匹配.具體原因寫寫有點麻煩,可以看tornado/gen.py 的_make_coroutine_wrapper
里處理stack_context.StackContextInconsistentError
的代碼(看代碼很難看出原因,用調試器跟蹤下執行流程,就會明白原因的,應該是tornado之前的bug).run_with_stack_context
就是torndao專門封裝用于處理協程函數的(其實就是bug修復函數), 不過這函數有點坑爹,如果你的協程函數要傳參的話,要用偏函數或者自己寫個run_with_stack_context
(這玩意就2行代碼)
終章
終于把這玩意寫完了...