起源
對(duì)tornado的StackContext的研究起源于一個(gè)優(yōu)化問(wèn)題.后來(lái)研究討論的優(yōu)化方案,需要修改每一個(gè)函數(shù)入?yún)?OMG)或者只需協(xié)程安全的全局變量.
但是tornado的協(xié)程只是個(gè)抽象概念,沒(méi)有實(shí)體.比如線程要實(shí)現(xiàn)這個(gè),有個(gè)threading local就可以(可以放置線程獨(dú)立的全局資源).如果協(xié)程也有類(lèi)似的功能就完美了,所以一個(gè)StackContent出現(xiàn)了.這貨是什么出身,參考tornnado/ stack_context.py的第一段注釋:
`StackContext` allows applications to maintain threadlocal-like state that follows execution
as it moves to other execution contexts.
是的,沒(méi)錯(cuò),這貨就是用來(lái)維護(hù)協(xié)程的上下文, 以實(shí)現(xiàn)協(xié)程的'threadlocal'功能.
研究
StackContext的核心代碼就在tornnado/stack_context.py中. 下面主要分析其中的幾個(gè)核心部分.
≌纸伞1. 首先映入我們眼簾的是
class _State(threading.local):
def __init__(self):
self.contexts = (tuple(), None)
_state = _State()
暫時(shí)不用太關(guān)心實(shí)現(xiàn)細(xì)節(jié)(本來(lái)也沒(méi)多少細(xì)節(jié)好不).主要說(shuō)明的是_state是線程獨(dú)立(因?yàn)槔^承于threading.local).然后簡(jiǎn)單的回顧下知識(shí)點(diǎn):一個(gè)進(jìn)程可以有多個(gè)線程,但是對(duì)于單核cpu,同一時(shí)間只能有一個(gè)線程在執(zhí)行,當(dāng)某一個(gè)線程執(zhí)行時(shí),寄存器的狀態(tài),特有數(shù)據(jù)的狀態(tài)(threading.local)等等組成了他執(zhí)行的上下文環(huán)境.線程不停切換時(shí),上下文也在不停的切換.現(xiàn)在針對(duì)協(xié)程,我們做個(gè)映射, 把進(jìn)程映射為線程,把線程映射為協(xié)程. _state就是用來(lái)指向當(dāng)前執(zhí)行協(xié)程的上下文環(huán)境.(應(yīng)該還沒(méi)暈吧)
2.我們繼續(xù)拾級(jí)而上,看到的是:
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就是我所說(shuō)的上下文對(duì)象了.但是是時(shí)候坦白了,這貨其實(shí)只是個(gè)跑腿的.真正管理協(xié)程上下文的是
def __init__(self, context_factory):
self.context_factory = context_factory
context_factory 可以理解為一個(gè)上下文的管理者,由它來(lái)生成一個(gè)真正的上下文context, context控制一個(gè)協(xié)程上下文的創(chuàng)建和退出.在協(xié)程切換時(shí),StackContext會(huì)告訴前一個(gè)context你被開(kāi)除了(__exit__
),并告訴context_factory趕緊給我找個(gè)新的context, 項(xiàng)目就要開(kāi)工了(__enter__
).
于是下面這兩個(gè)函數(shù)就很好理解了:
def enter(self):
context = self.context_factory()
self.contexts.append(context)
context.__enter__()
就是先用context_factory上下文管理者生成一個(gè)上下文,然后保存該上下文(退出時(shí)用),最后進(jìn)入了該上下文
def exit(self, type, value, traceback):
context = self.contexts.pop()
context.__exit__(type, value, traceback)
這就是先取出最后的一個(gè)上下文,然后退出.
這就是StackContext,context_factory,context三者的關(guān)系了.
上面說(shuō)明了StackContext對(duì)于協(xié)程上下文的創(chuàng)建和摧毀,下面說(shuō)明下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因?yàn)槭菞J缴舷挛?所以__enter__
里面干的活就是:先保存現(xiàn)有的上下文,再將自己放入上下文堆棧的棧頂,最后重新設(shè)置當(dāng)前的上下文環(huán)境.
3.最后高潮即將來(lái)臨:我們先總結(jié)下:
- _state用來(lái)指向當(dāng)前運(yùn)行協(xié)程的上下文的,協(xié)程不斷切換過(guò)程中,_state也指向不同的上下文.
- StackContext負(fù)責(zé)上下文切換的具體工作,即退出之前的上下文,進(jìn)入新的上下文,忙成狗的角色.
有個(gè)這兩個(gè)對(duì)象,最后看下wrap函數(shù)(簡(jiǎn)化后):
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
這里有兩種上下文:定義時(shí)上下文和執(zhí)行時(shí)上下文.定義時(shí)上下文是協(xié)程函數(shù)定義時(shí)指定的上下文, 運(yùn)行時(shí)上下文是協(xié)程函數(shù)運(yùn)行時(shí)系統(tǒng)所處的上下文.即協(xié)程函數(shù)定義時(shí)說(shuō),我要在有空調(diào),有可樂(lè)的環(huán)境下工作,但是系統(tǒng)在不停切換后,切換到那個(gè)協(xié)程時(shí),系統(tǒng)環(huán)境只有個(gè)破風(fēng)扇在轉(zhuǎn)著.
協(xié)程在這種情況下,只能自己創(chuàng)造自己喜歡的環(huán)境了(將運(yùn)行時(shí)環(huán)境改造成定義說(shuō)明的環(huán)境).當(dāng)初研究到這我有點(diǎn)想不通,定義時(shí)的環(huán)境如何一直保存著呢?答案是通過(guò)閉包.
現(xiàn)在再看這個(gè)函數(shù)時(shí),就比較好理解了, cap_contexts = [_state.contexts]
就是將定義時(shí)上下文保存到了cap_contexts
.而wrapped
就是我們最終扔給IoLoop
的協(xié)程函數(shù)了.wrapped
具體什么時(shí)候執(zhí)行,執(zhí)行時(shí)候的_state是什么,都是不確定的,所以wrapped
主要工作就是將cap_contexts
保存的上下文,替換到當(dāng)前上下文中.
下面基本分析下流程:
cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
移除定義時(shí)有效,但是執(zhí)行時(shí)已經(jīng)無(wú)效的上下文.
for n in stack:
try:
n.enter()
last_ctx += 1
except:
pass
每個(gè)stack里面的元素的就是StackContext對(duì)象,也即按從棧底到棧頂?shù)捻樞?逐一恢復(fù)到定義時(shí)上下文環(huán)境.
if top is None:
try:
ret = fn(*args, **kwargs)
如果恢復(fù)時(shí)沒(méi)有異常,才開(kāi)始執(zhí)行真正的協(xié)程代碼
while last_ctx > 0:
last_ctx -= 1
c = stack[last_ctx]
try:
c.exit(*exc)
協(xié)程函數(shù)執(zhí)行結(jié)束后,按相反的順序退出棧式上下文.
基本上大致流程就是這樣了.
wrap
函數(shù)主要的使用場(chǎng)景就是將協(xié)程函數(shù)放入?yún)f(xié)程引擎前(IOLoop),加上一層上下文管理功能.具體可參加tornado/ioloop.py的PollIOLoop.add_callback
應(yīng)用
以上扯了這么多,其實(shí)就是說(shuō)明了tornado協(xié)程上下文切換的大體機(jī)制,但是具體的上下文還是需要自己實(shí)現(xiàn),而實(shí)現(xiàn)的關(guān)鍵就是context_factory
.
github上就有人實(shí)現(xiàn)了一個(gè)context_factory 地址是:https://github.com/viewfinderco/viewfinder/blob/master/backend/base/context_local.py.
只要寫(xiě)個(gè)子類(lèi)繼承下里面的ContextLocal
, 你就擁有了一個(gè)context_factory
,按他文章的例子,定義一個(gè)子類(lèi)MyContext, 那么就可以按如下代碼使用:
yield run_with_stack_context(StackContext(MyContext(coroutine_value)), your_func)
這里需要說(shuō)明幾點(diǎn):
1 coroutine就相當(dāng)于協(xié)程獨(dú)立的變量,就是我們最終想要的功能,可以實(shí)現(xiàn)一個(gè)管理協(xié)程資源的類(lèi),然后將他的實(shí)例傳遞進(jìn)去.
2 上面這個(gè)寫(xiě)法只是針對(duì)your_func是協(xié)程函數(shù)的情況,如果針對(duì)普通函數(shù),只需:
with StackContext(MyContext(coroutine_value)):
your_func()
3 為什么針對(duì)協(xié)程函數(shù)會(huì)這么特別,這是因?yàn)橹苯佑闷毡楹瘮?shù)的調(diào)用方法會(huì)導(dǎo)致下上文堆棧不匹配.具體原因?qū)憣?xiě)有點(diǎn)麻煩,可以看tornado/gen.py 的_make_coroutine_wrapper
里處理stack_context.StackContextInconsistentError
的代碼(看代碼很難看出原因,用調(diào)試器跟蹤下執(zhí)行流程,就會(huì)明白原因的,應(yīng)該是tornado之前的bug).run_with_stack_context
就是torndao專(zhuān)門(mén)封裝用于處理協(xié)程函數(shù)的(其實(shí)就是bug修復(fù)函數(shù)), 不過(guò)這函數(shù)有點(diǎn)坑爹,如果你的協(xié)程函數(shù)要傳參的話,要用偏函數(shù)或者自己寫(xiě)個(gè)run_with_stack_context
(這玩意就2行代碼)
終章
終于把這玩意寫(xiě)完了...