tornado stackcontext解析

起源

對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行代碼)

終章

終于把這玩意寫完了...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容