tornado stackcontext解析

起源

對(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ě)完了...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末镜豹,一起剝皮案震驚了整個(gè)濱河市挂滓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異匾竿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蔚万,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)岭妖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事昵慌〖俣幔” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵斋攀,是天一觀的道長(zhǎng)已卷。 經(jīng)常有香客問(wèn)我,道長(zhǎng)淳蔼,這世上最難降的妖魔是什么侧蘸? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮鹉梨,結(jié)果婚禮上讳癌,老公的妹妹穿的比我還像新娘。我一直安慰自己俯画,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布司草。 她就那樣靜靜地躺著艰垂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪埋虹。 梳的紋絲不亂的頭發(fā)上猜憎,一...
    開(kāi)封第一講書(shū)人閱讀 51,462評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音搔课,去河邊找鬼胰柑。 笑死爬泥,一個(gè)胖子當(dāng)著我的面吹牛柬讨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播袍啡,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼踩官,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了境输?” 一聲冷哼從身側(cè)響起蔗牡,我...
    開(kāi)封第一講書(shū)人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嗅剖,沒(méi)想到半個(gè)月后辩越,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡信粮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年黔攒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡亏钩,死狀恐怖莲绰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情姑丑,我是刑警寧澤蛤签,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站栅哀,受9級(jí)特大地震影響震肮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜留拾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一戳晌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痴柔,春花似錦沦偎、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至谈火,卻和暖如春侈询,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背糯耍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工扔字, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人温技。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓革为,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親舵鳞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子篷角,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容