索引
本節(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
將會在Future
被set_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)用了futrue
的result
方法, 那么異常就會被觸發(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-異步客戶端