譯者說
Tornado 4.3
于2015年11月6日發(fā)布旋奢,該版本正式支持Python3.5
的async
/await
關(guān)鍵字,并且用舊版本CPython編譯Tornado同樣可以使用這兩個關(guān)鍵字沽瘦,這無疑是一種進(jìn)步革骨。其次,這是最后一個支持Python2.6
和Python3.2
的版本了析恋,在后續(xù)的版本了會移除對它們的兼容×颊埽現(xiàn)在網(wǎng)絡(luò)上還沒有Tornado4.3
的中文文檔,所以為了讓更多的朋友能接觸并學(xué)習(xí)到它助隧,我開始了這個翻譯項(xiàng)目筑凫,希望感興趣的小伙伴可以一起參與翻譯,項(xiàng)目地址是tornado-zh on Github并村,翻譯好的文檔在Read the Docs上直接可以看到巍实。歡迎Issues or PR。
協(xié)程
Tornado中推薦使用協(xié)程寫異步代碼. 協(xié)程使用了Python的yield
關(guān)鍵字代替鏈?zhǔn)交卣{(diào)來將程序掛起和恢復(fù)執(zhí)行(像在 gevent中出現(xiàn)的輕量級線程合作方式有時也被稱為協(xié)程,但是在Tornado中所有的協(xié)程使用明確的上下文切換,并被稱為異步函數(shù)).
使用協(xié)程幾乎像寫同步代碼一樣簡單, 并且不需要浪費(fèi)額外的線程. 它們還通過減少上下文切換來 使并發(fā)編程更簡單.
例子:
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
# 在Python 3.3之前, 在generator中是不允許有返回值的
# 必須通過拋出異常來代替.
# 就像 raise gen.Return(response.body).
return response.body
Python 3.5:async
和await
Python 3.5 引入了async
和await
關(guān)鍵字(使用這些關(guān)鍵字的函數(shù)也被稱為"原生協(xié)程"). 從Tornado 4.3,你可以用它們代替yield
為基礎(chǔ)的協(xié)程.只需要簡單的使用async def foo()
在函數(shù)定義的時候代替@gen.coroutine
裝飾器, 用await
代替yield. 本文檔的其他部分會繼續(xù)使用yield
的風(fēng)格來和舊版本的Python兼容, 但是如果async
和await
可用的話哩牍,它們運(yùn)行起來會更快:
async def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
await
關(guān)鍵字比 yield
關(guān)鍵字功能要少一些.例如,在一個使用 yield
的協(xié)程中棚潦, 你可以得到Futures
列表, 但是在原生協(xié)程中,你必須把列表用 tornado.gen.multi
包起來. 你也可以使用 tornado.gen.convert_yielded
來把任何使用yield
工作的代碼轉(zhuǎn)換成使用await
的形式.
雖然原生協(xié)程沒有明顯依賴于特定框架(例如它們沒有使用裝飾器,例如tornado.gen.coroutine
或asyncio.coroutine
), 不是所有的協(xié)程都和其他的兼容. 有一個coroutine runner在第一個協(xié)程被調(diào)用的時候進(jìn)行選擇, 然后被所有用await
直接調(diào)用的協(xié)程共享. Tornado的協(xié)程執(zhí)行者(coroutine runner)在設(shè)計(jì)上是多用途的,可以接受任何來自其他框架的awaitable對象;其他的協(xié)程運(yùn)行時可能有很多限制(例如,asyncio
協(xié)程執(zhí)行者不接受來自其他框架的協(xié)程).基于這些原因,我們推薦組合了多個框架的應(yīng)用都使用Tornado的協(xié)程執(zhí)行者來進(jìn)行協(xié)程調(diào)度.為了能使用Tornado來調(diào)度執(zhí)行asyncio的協(xié)程, 可以使用tornado.platform.asyncio.to_asyncio_future
適配器.
它是如何工作的
包含了yield
關(guān)鍵字的函數(shù)是一個生成器(generator). 所有的生成器都是異步的; 當(dāng)調(diào)用它們的時候,會返回一個生成器對象,而不是一個執(zhí)行完的結(jié)果.@gen.coroutine
裝飾器通過yield
表達(dá)式和生成器進(jìn)行交流, 而且通過返回一個.Future
與協(xié)程的調(diào)用方進(jìn)行交互.
下面是一個協(xié)程裝飾器內(nèi)部循環(huán)的簡單版本:
# tornado.gen.Runner 簡化的內(nèi)部循環(huán)
def run(self):
# send(x) makes the current yield return x.
# It returns when the next yield is reached
future = self.gen.send(self.next)
def callback(f):
self.next = f.result()
self.run()
future.add_done_callback(callback)
裝飾器從生成器接收一個.Future
對象, 等待(非阻塞的)這個.Future
對象執(zhí)行完成, 然后"解開(unwraps)"這個.Future
對象,并把結(jié)果作為yield
表達(dá)式的結(jié)果傳回給生成器.大多數(shù)異步代碼從來不會直接接觸.Future
類.除非 .Future
立即通過異步函數(shù)返回給yield
表達(dá)式.
如何調(diào)用協(xié)程
協(xié)程一般不會拋出異常: 它們拋出的任何異常將被.Future
捕獲直到它被得到.這意味著用正確的方式調(diào)用協(xié)程是重要的, 否則你可能有被忽略的錯誤:
@gen.coroutine
def divide(x, y):
return x / y
def bad_call():
# 這里應(yīng)該拋出一個 ZeroDivisionError 的異常, 但事實(shí)上并沒有
# 因?yàn)閰f(xié)程的調(diào)用方式是錯誤的.
divide(1, 0)
幾乎所有的情況下, 任何一個調(diào)用協(xié)程的函數(shù)都必須是協(xié)程它自身, 并且在調(diào)用的時候使用yield
關(guān)鍵字. 當(dāng)你復(fù)寫超類中的方法, 請參閱文檔,看看協(xié)程是否支持(文檔應(yīng)該會寫該方法 "可能是一個協(xié)程" 或者 "可能返回一個 .Future
"):
@gen.coroutine
def good_call():
# yield 將會解開 divide() 返回的 Future 并且拋出異常
yield divide(1, 0)
有時你可能想要對一個協(xié)程"一勞永逸"而且不等待它的結(jié)果. 在這種情況下,建議使用.IOLoop.spawn_callback
, 它使得.IOLoop
負(fù)責(zé)調(diào)用. 如果它失敗了, .IOLoop
會在日志中把調(diào)用棧記錄下來:
# IOLoop 將會捕獲異常,并且在日志中打印棧記錄.
# 注意這不像是一個正常的調(diào)用, 因?yàn)槲覀兪峭ㄟ^
# IOLoop 調(diào)用的這個函數(shù).
IOLoop.current().spawn_callback(divide, 1, 0)
最后, 在程序頂層, 如果.IOLoop
尚未運(yùn)行, 你可以啟動.IOLoop
,執(zhí)行協(xié)程,然后使用.IOLoop.run_sync
方法停止.IOLoop
. 這通常被用來啟動面向批處理程序的main
函數(shù):
# run_sync() 不接收參數(shù),所以我們必須把調(diào)用包在lambda函數(shù)中.
IOLoop.current().run_sync(lambda: divide(1, 0))
協(xié)程模式
結(jié)合 callback
為了使用回調(diào)代替.Future
與異步代碼進(jìn)行交互, 把調(diào)用包在.Task
類中. 這將為你添加一個回調(diào)參數(shù)并且返回一個可以yield的.Future
:
@gen.coroutine
def call_task():
# 注意這里沒有傳進(jìn)來some_function.
# 這里會被Task翻譯成
# some_function(other_args, callback=callback)
yield gen.Task(some_function, other_args)
調(diào)用阻塞函數(shù)
從協(xié)程調(diào)用阻塞函數(shù)最簡單的方式是使用concurrent.futures.ThreadPoolExecutor
, 它將返回和協(xié)程兼容的Futures
:
thread_pool = ThreadPoolExecutor(4)
@gen.coroutine
def call_blocking():
yield thread_pool.submit(blocking_func, args)
并行
協(xié)程裝飾器能識別列表或者字典對象中各自的 Futures
, 并且并行的等待這些 Futures
:
@gen.coroutine
def parallel_fetch(url1, url2):
resp1, resp2 = yield [http_client.fetch(url1),
http_client.fetch(url2)]
@gen.coroutine
def parallel_fetch_many(urls):
responses = yield [http_client.fetch(url) for url in urls]
# 響應(yīng)是和HTTPResponses相同順序的列表
@gen.coroutine
def parallel_fetch_dict(urls):
responses = yield {url: http_client.fetch(url)
for url in urls}
# 響應(yīng)是一個字典 {url: HTTPResponse}
交叉存取
有時候保存一個 .Future
比立即yield它更有用, 所以你可以在等待之前
執(zhí)行其他操作:
@gen.coroutine
def get(self):
fetch_future = self.fetch_next_chunk()
while True:
chunk = yield fetch_future
if chunk is None: break
self.write(chunk)
fetch_future = self.fetch_next_chunk()
yield self.flush()
循環(huán)
協(xié)程的循環(huán)是棘手的, 因?yàn)樵赑ython中沒有辦法在for
循環(huán)或者while
循環(huán)yield
迭代器,并且捕獲yield的結(jié)果. 相反,你需要將循環(huán)條件從訪問結(jié)果中分離出來, 下面是一個使用Motor的例子:
import motor
db = motor.MotorClient().test
@gen.coroutine
def loop_example(collection):
cursor = db.collection.find()
while (yield cursor.fetch_next):
doc = cursor.next_object()
在后臺運(yùn)行
PeriodicCallback
通常不使用協(xié)程. 相反,一個協(xié)程可以包含一個while True:
循環(huán)并使用tornado.gen.sleep
:
@gen.coroutine
def minute_loop():
while True:
yield do_something()
yield gen.sleep(60)
# Coroutines that loop forever are generally started with
# spawn_callback().
IOLoop.current().spawn_callback(minute_loop)
有時可能會遇到一個更復(fù)雜的循環(huán). 例如, 上一個循環(huán)運(yùn)行每次花費(fèi)60+N
秒,其中N
是do_something()
花費(fèi)的時間. 為了準(zhǔn)確的每60秒運(yùn)行,使用上面的交叉模式:
@gen.coroutine
def minute_loop2():
while True:
nxt = gen.sleep(60) # 開始計(jì)時.
yield do_something() # 計(jì)時后運(yùn)行.
yield nxt # 等待計(jì)時結(jié)束.