Tornado應(yīng)用筆記04-淺析源碼

索引

本節(jié)內(nèi)容將分析Tornado中利用協(xié)程實(shí)現(xiàn)異步原理, 主要分析的是裝飾器@gen.coroutine, 包括源碼分析和異常捕獲等問題, 另外也包括了對@asynchronous, Future等相關(guān)對象的分析.

"未來的坑" Future

在介紹兩個重要的裝飾器之前, 先來說說Future, 它是實(shí)現(xiàn)異步的一個重要對象. Future就像它的名字一樣, 裝載的是"未來"(未完成操作的結(jié)果), 文檔描述它是"異步操作結(jié)果的占位符". 在Tornado中, 常見用法有下面兩種:

# 在`IOLoop`注冊`future`
tornado.ioloop.IOLoop.add_future(future, future_done_callback_func)

# `@gen.coroutine`內(nèi)部結(jié)合`yield`使用
@gen.coroutine
def foo():
    result = yield future

Tornado中內(nèi)置的Future(tornado.concurrent.Future)與futures包中的Future(concurrent.futures.Future)很相似, 不過Tornado的Future不是"線程安全"的, 因?yàn)門ornado本身是單線程, 所以用起來并沒什么不妥, 而且速度更快

Tornado 4.0以前, Tornado的Future實(shí)際上還是引用的"線程安全"的concurrent.futures.Future, 只有在沒有安裝future包時(shí)才會使用"非線程安全"的Tornado 內(nèi)置Future. Tornado 4.0以后的版本, 所有的Future都變成內(nèi)置的, 并為其加入了exc_info方法. 這兩種Futrue基本上是"兼容"的, 不過這里所謂的"兼容"只是在"調(diào)用"層面上的, 部分操作不一定會生效或執(zhí)行.

Tornado 4.1后, 如果Future中的異常沒有被觸發(fā)(比如調(diào)用result(),exception()exc_info()), 那在Future被垃圾回收時(shí), 會log異常信息. 如果你既想"發(fā)現(xiàn)"異常, 又不想讓它log, 可以這么做future.add_done_callback(lambda future: future.exception())

下面介紹Future中最主要三個方法:

class Future(object):

    def result(self, timeout=None):
        # 返回future的值(future._result), 如果有執(zhí)行異常, 那么將會觸發(fā)異常
        self._clear_tb_log()
        if self._result is not None:
            return self._result
        if self._exc_info is not None:
            raise_exc_info(self._exc_info)
        self._check_done()
        return self._result

    def add_done_callback(self, fn):
        # 為future添加回調(diào)到回調(diào)列表中, 在`.set_result`后執(zhí)行,
        # 不過如果future已經(jīng)完成了, 那么會直接執(zhí)行這個回調(diào), 不放入回調(diào)列表
        if self._done:
            fn(self)
        else:
            self._callbacks.append(fn)

    def set_result(self, result):
        # 為future設(shè)置值, 然后執(zhí)行回調(diào)列表中的所有回調(diào), 
        # 回調(diào)傳入的唯一參數(shù)就是future本身
        self._result = result
        self._set_done()

Future對于剛開始接觸這個問題的開發(fā)者來說, 可能是一個不容易理解的對象, 是需要一定時(shí)間的去消化. 雖然你可能在之前已經(jīng)借助gen.coroutine@asynchronous寫過一些異步代碼, 但是Future都是被封裝到里邊的, 你并不清楚其中的原理. 當(dāng)你看到一些更靈活的異步應(yīng)用時(shí), 你可能就沒有辦法理解其中的邏輯. 所以Tornado作者建議大家都用Future練習(xí)寫異步代碼, 以便更好理解其所以然.

下面的例子實(shí)現(xiàn)了異步HTTP請求, 一個用@gen.coroutine實(shí)現(xiàn), 一個用較原始的Future實(shí)現(xiàn), 對比其中的不同, 或者動手改改, 但愿能幫助你理解Future.

# @gen.coroutine 實(shí)現(xiàn)
class AsyncFetch(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self, *args, **kwargs):
        client = tornado.httpclient.AsyncHTTPClient()
        response = yield client.fetch('http://www.baidu.com', request_timeout=2)
        self.finish(response.body)

# Future 實(shí)現(xiàn)
class AsyncFetch(tornado.web.RequestHandler):
    @asynchronous
    def get(self, *args, **kwargs):
        client = tornado.httpclient.AsyncHTTPClient()
        fetch_future = client.fetch('http://www.baidu.com', request_timeout=2)
        tornado.ioloop.IOLoop.current().add_future(fetch_future, callback=self.on_response)

    def on_response(self, future):
        response = future.result()
        self.finish(response .body)
異步裝飾器 @asynchronous

這個裝飾器適合處理回調(diào)式的異步操作, 如果你想使用協(xié)程實(shí)現(xiàn)異步, 那么應(yīng)該單獨(dú)使用@gen.coroutine. 考慮到某些歷史遺留問題, 同時(shí)使用 @gen.coroutine@asynchronous也是可以的, 但是 @asynchronous 必須放在 @gen.coroutine的前面, 否則@asynchronous將被忽略.

注意, 這個裝飾器能且只能用在get post一類方法上, 用在其他任意方法都是無意義的. 同時(shí)裝飾器并不會"真正"使一個請求變?yōu)楫惒? 而僅僅是"告訴"tornado這個請求是異步的, 要使請求異步化, 則必須要在請求內(nèi)完成一些異步操作, 里面的阻塞操作是會阻塞整個線程的, 不會響應(yīng)新的請求, 如果你在里面sleep了, 那線程就sleep了.

另外, 用了這個裝飾器以后, 請求并不會在return后結(jié)束(因?yàn)檫@個請求是異步的, Tornado"不知道"何時(shí)會完成, 所以會一直保持與客戶端的連接), 需要顯式調(diào)用 self.finish() 才會結(jié)束請求

附: Tornado 作者對 @gen.coroutine@asynchronous 一起使用的回答:

Order matters because @asynchronous looks at the Future returned by @gen.coroutine, and calls finish for you when the coroutine returns. Since Tornado 3.1, the combination of @asynchronous and @gen.coroutine has been unnecessary and discouraged; in most cases you should use @gen.coroutine alone.

@gen.coroutine@asynchronous共用需要注意順序是因?yàn)? @asynchronous監(jiān)控著@gen.coroutine 返回的 Future 然后在Future完成的時(shí)候自動調(diào)用 finish.自tornado 3.1開始, 兩者就可以獨(dú)立使用且并不鼓勵共用, 實(shí)際上在絕大多數(shù)情況下,只需要使用 @gen.coroutine

源碼注釋:
@functools.wraps(method)
def wrapper(self, *args, **kwargs):

    # 關(guān)閉自動finish, 需要顯式調(diào)用self.finish()
    self._auto_finish = False
    with stack_context.ExceptionStackContext(
            self._stack_context_handle_exception):

        # 執(zhí)行method內(nèi)的函數(shù), 并將結(jié)果轉(zhuǎn)換成future, 
        # 使用`add_future`將回調(diào)函數(shù)`future_complete`注冊到`ioloop`中,
        # 回調(diào)做了兩件事, 一是通過調(diào)用`future.result()`檢查異常
        # 二是自動finish請求, 無需在請求內(nèi)顯式finish
        result = method(self, *args, **kwargs)
        if result is not None:
            result = gen.convert_yielded(result)
            def future_complete(f):
                f.result()
                if not self._finished:
                    self.finish()
            IOLoop.current().add_future(result, future_complete)
            return None
        return result
return wrapper

協(xié)程裝飾器 @gen.coroutine

在理解這個裝飾器前, 需要你已經(jīng)了解生成器的工作方式, 比如看懂下面這段代碼和執(zhí)行結(jié)果. 如果你對此還不了解, 那么建議你先看看這篇文章, 然后再往下讀.

>>> def echo(value=None):
...   while 1:
...     value = (yield value)
...     print("The value is", value)
...     if value:
...       value += 1
...
>>> g = echo(1)
>>> next(g)
1
>>> g.send(2)
The value is 2
3
>>> g.send(5)
The value is 5
6
>>> next(g)
The value is None

Py 3.3以前的版本, 使用了這個裝飾器的生成器(含yield的函數(shù))都不能直接使用return來返回值, 需要觸發(fā)一種特殊的異常gen.Return來達(dá)到return的效果, 不過在任意版本中均可通過不帶參數(shù)的return提前退出生成器.

裝飾器返回的是一個Future對象, 如果調(diào)用時(shí)設(shè)置了回調(diào)函數(shù)callback, 那么callback將會在Futureset_result后調(diào)用, 若協(xié)程執(zhí)行失敗, callback也不會執(zhí)行. 需要注意的是, callback并不需要作為被修飾函數(shù)的"可見"參數(shù), 因?yàn)?code>callback是被gen.coroutine處理的(具體用法見上一節(jié)線程池處理阻塞操作部分).

需要特別注意的是其中的異常處理. 執(zhí)行發(fā)生異常時(shí), 異常信息會存儲在.Future 對象內(nèi). 所以必須檢查.Future 對象的結(jié)果, 否則潛在的異常將被忽略. 在一個@gen.coroutine內(nèi)調(diào)用另外一個@gen.coroutine, 官方文檔推薦兩種方式

# 在頂層使用下面的方式調(diào)用
tornado.ioloop.IOLoop.run_sync(coroutine_task_func)

# 使用`add_future`
tornado.ioloop.IOLoop.add_future(future, callback)

其實(shí)實(shí)際上只要調(diào)用了futrueresult方法, 那么異常就會被觸發(fā), 所以也可以使用下面兩種方式

# 使用了`@gen.coroutine`的生成器, 靠`Runner`調(diào)用`future.result`觸發(fā)異常, 下面會分析`Runner`源碼
yield tornado.gen.Task(coroutine_task_func)
yield the_coroutine_task(callback=my_callback_func)
異常捕獲
@tornado.gen.coroutine
def catch_exc():
    r = yield tornado.gen.sleep(0.1)
    raise KeyError


@tornado.gen.coroutine
def uncatch_exc():
    # 需要注意的是, 這里的阻塞操作, 也是會阻塞整個線程的
    time.sleep(0.1)
    raise KeyError


class CoroutineCatchExc(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        # 直接調(diào)用 `catch_exc` 也是可以觸發(fā)異常的, 不過無法在這里捕獲
        # 因?yàn)槔锩嬗? 在 `Runner` 中對生成器 `send` 操作的時(shí)候會觸發(fā)
        # 不過如果只是想丟到`后臺`執(zhí)行, 這樣做也是可以的, 異常都交給任務(wù)自身處理
        catch_exc()

        # 如果單獨(dú)使用下面的調(diào)用是會徹底忽略掉協(xié)程執(zhí)行中的異常的, 不會輸出任何信息,
        uncatch_exc()

        # 下面的用法也會觸發(fā)異常, 不過同樣的, 并沒有辦法在這里捕獲
        # gen.coroutine 在調(diào)用 callback 時(shí)自動傳入 future.result(), 拋出異常
        uncatch_exc(callback=lambda future_result: -1)

        # 捕獲并處理異常的方法

        # 方法1
        # 需要注意的是使用`ioloop`回調(diào)傳入的是`future`, 不是`future.result()`
        # 所以, 在回調(diào)里面不調(diào)用`future.result()`也是白搭
        def foo(future):
            fu = future  # 這樣做也是觸發(fā)不了異常的
            try:
                future_result = fu.result()  # 這樣才可以
            except:
                import traceback
                print 'catch exc in callback, the exc info is:'
                print traceback.format_exc()
            else:
                print 'future completed and the result is %s' % future_result
        
        fu = uncatch_exc()
        tornado.ioloop.IOLoop.current().add_future(fu, callback=foo)

        # 方法2
        # 使用 yield 后, 就成了生成器, 在`gen.coroutine`中會調(diào)用`Runner`
        # 驅(qū)動生成器, `Runner`內(nèi)部有調(diào)用`future.result()`
        try:
            future_result = yield uncatch_exc('catch exc')
        except:
            import traceback
            print 'catch exc in yield, the exc info is:'
            print traceback.format_exc()
        else:
            print 'future completed and the result is %s' % future_result

        self.finish("coroutine catch exc test")

源碼注釋:
def coroutine(func, replace_callback=True):
    # `coroutine`的功能實(shí)際上由`_make_coroutine_wrapper`實(shí)現(xiàn)
    return _make_coroutine_wrapper(func, replace_callback=True)

def _make_coroutine_wrapper(func, replace_callback):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 創(chuàng)建一個 `future`
        future = TracebackFuture()

        # 如果調(diào)用時(shí)設(shè)置了`callback`, 則在`IOLoop`注冊`future`及其回調(diào)事件
        # 因?yàn)楸恍揎椀暮瘮?shù)沒有`callback`這個"可見"參數(shù), 所以需要`pop`掉, 以免報(bào)錯
        if replace_callback and 'callback' in kwargs:
            callback = kwargs.pop('callback')
            IOLoop.current().add_future(
                future, lambda future: callback(future.result()))

        # 執(zhí)行被修飾函數(shù), 獲取結(jié)果
        # 拋出的執(zhí)行異常將被`set_exc_info`進(jìn)`Future`內(nèi), 在執(zhí)行`future.result()`時(shí), 異常會被觸發(fā),
        # 對于`Return`和`StopIteration`, 這類特殊的異常, 將返回函數(shù)的返回值
        # 不過在`Python 3.3+`中`StopIteration`才會有`value`屬性, 也就是可以直接使用`return`返回
        try:
            result = func(*args, **kwargs)
        except (Return, StopIteration) as e:
            result = _value_from_stopiteration(e)
        except Exception:
            future.set_exc_info(sys.exc_info())
            return future

        # 這里使用的`else`只有在`try`正常結(jié)束時(shí)執(zhí)行
        # 如果被修飾的是一個`生成器`, 獲取生成器生成的第一個結(jié)果, 異常處理與上面一致
        # 如果只是普通的"同步"函數(shù)(不是生成器), 那就跳過這步, 避免創(chuàng)建`Runner`浪費(fèi)資源
        # 將第一個`yield`的結(jié)果, `生成器`(函數(shù)本身)和上面新建的`Future`一同傳入`Runner`
        # `Runner`是實(shí)現(xiàn)協(xié)程異步的關(guān)鍵, 下面接著分析其中的代碼
        else:
            if isinstance(result, GeneratorType):
                try:
                    orig_stack_contexts = stack_context._state.contexts
                    yielded = next(result)
                    if stack_context._state.contexts is not orig_stack_contexts:
                        yielded = TracebackFuture()
                        yielded.set_exception(
                            stack_context.StackContextInconsistentError(
                                'stack_context inconsistency (probably caused '
                                'by yield within a "with StackContext" block)'))
                except (StopIteration, Return) as e:
                    future.set_result(_value_from_stopiteration(e))
                except Exception:
                    future.set_exc_info(sys.exc_info())
                else:
                    Runner(result, future, yielded)
                try:
                    # 生成器, 經(jīng)過`Runner`, 已經(jīng)`set_result`, 直接返回`future`
                    return future
                finally:
                    future = None
        # 非生成器, 沒經(jīng)過`Runner`, `set_result`后返回
        future.set_result(result)
        return future
    return wrapper

# `Runner`主要看`run`和`handle_yield`兩個函數(shù)
class Runner(object):
    def __init__(self, gen, result_future, first_yielded):
        self.gen = gen
        self.result_future = result_future
        self.future = _null_future
        # 將結(jié)果轉(zhuǎn)換成future, 然后判斷狀態(tài), 擇機(jī)進(jìn)入run
        if self.handle_yield(first_yielded):
            self.run()

    # `run`實(shí)際上就是一個生成器驅(qū)動器, 與`IOLoop.add_future`配合, 利用協(xié)程實(shí)現(xiàn)異步
    # `run`內(nèi)部雖然是個死循環(huán), 但是因?yàn)檎{(diào)用了`gen.send`, 
    # 所以在`gen.send`時(shí)可以暫時(shí)離開循環(huán), 返回到生成器中(即yield的`斷點(diǎn)`), 使得生成器得以繼續(xù)工作
    # 當(dāng)生成器返回一個新的`future`時(shí), 再次調(diào)用`handle_yield`, 
    # 若`future`完成了就進(jìn)入下一次`yield`, 
    # 沒完成就等到完成以后在進(jìn)入到`run`進(jìn)入下一次`yield`
   
    # 簡化的`run`可表示成下面的樣子
    # def run(self):
    #    future = self.gen.send(self.next)
    #    def callback(f):
    #        self.next = f.result()
    #        self.run()
    #    future.add_done_callback(callback)

    def run(self):
        # 各種運(yùn)行狀態(tài)判斷, 異常處理
        if self.running or self.finished:
            return
        try:
            self.running = True
            while True:
                future = self.future
                if not future.done():
                    return
                self.future = None
                try:
                    orig_stack_contexts = stack_context._state.contexts
                    exc_info = None

                    # 查異常, 有則拋出
                    try:
                        value = future.result()
                    except Exception:
                        self.had_exception = True
                        exc_info = sys.exc_info()

                    if exc_info is not None:
                        yielded = self.gen.throw(*exc_info)
                        exc_info = None

                    # 正常情況, 無異常
                    else:
                        # 驅(qū)動生成器運(yùn)行, 恢復(fù)到`yield`斷點(diǎn)繼續(xù)執(zhí)行, 是整個函數(shù)的關(guān)鍵
                        yielded = self.gen.send(value)

                    if stack_context._state.contexts is not orig_stack_contexts:
                        self.gen.throw(
                            stack_context.StackContextInconsistentError(
                                'stack_context inconsistency (probably caused '
                                'by yield within a "with StackContext" block)'))

                # 生成器被掏空, 結(jié)束
                except (StopIteration, Return) as e:
                    self.finished = True
                    self.future = _null_future
                    if self.pending_callbacks and not self.had_exception:
                        raise LeakedCallbackError(
                            "finished without waiting for callbacks %r" %
                            self.pending_callbacks)
                    self.result_future.set_result(_value_from_stopiteration(e))
                    self.result_future = None
                    self._deactivate_stack_context()
                    return

                # 其他異常
                except Exception:
                    self.finished = True
                    self.future = _null_future
                    self.result_future.set_exc_info(sys.exc_info())
                    self.result_future = None
                    self._deactivate_stack_context()
                    return
                # 配合`handle_yield`, 使用`IOLoop`注冊事件
                if not self.handle_yield(yielded):
                    return
        finally:
            self.running = False

    def handle_yield(self, yielded):
        # 省略部分無關(guān)代碼
        # 先將傳入的第一個生成器結(jié)果轉(zhuǎn)換為`Future`對象
        # 如果`Future`還沒有執(zhí)行完畢, 或者是`moment`(一種內(nèi)置的特殊`Future`, 這里可以忽視)
        # 那就等待`Future`執(zhí)行完畢后執(zhí)行`run`
        # 其余情況則直接執(zhí)行`run`
        ...
        if ...:
            ...
        else:
            try:
                self.future = convert_yielded(yielded)
            except BadYieldError:
                self.future = TracebackFuture()
                self.future.set_exc_info(sys.exc_info())

        if not self.future.done() or self.future is moment:
            self.io_loop.add_future(
                self.future, lambda f: self.run())
            return False
        return True
特殊函數(shù)gen.Task

gen.Task的操作就是將回調(diào)式異步函數(shù)的輸出轉(zhuǎn)換成future類型并返回, 目的是方便被yield. 函數(shù)會自動為執(zhí)行函數(shù)設(shè)置回調(diào), 回調(diào)的工作是將操作的返回值傳遞給內(nèi)部創(chuàng)建的future. 其代碼可以簡化為:

def Task(func, *args, **kwargs):
    future = Future()
    callback = lambda func_result: future.set_result(func_result)
    func(*args, callback=callback, **kwargs)
    return future

本節(jié)內(nèi)容就是這些, 下節(jié)內(nèi)容將討論Tornado內(nèi)置的異步HTTP客戶端.

NEXT ===> Tornado應(yīng)用筆記05-異步客戶端

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

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