Tornado 4.3
於2015年11月6日發佈,該版本正式支持Python3.5
的async
/await
關鍵字,而且用舊版本CPython編譯Tornado一樣可使用這兩個關鍵字,這無疑是一種進步。其次,這是最後一個支持Python2.6
和Python3.2
的版本了,在後續的版本了會移除對它們的兼容。如今網絡上尚未Tornado4.3
的中文文檔,因此爲了讓更多的朋友能接觸並學習到它,我開始了這個翻譯項目,但願感興趣的小夥伴能夠一塊兒參與翻譯,項目地址是tornado-zh on Github,翻譯好的文檔在Read the Docs上直接能夠看到。歡迎Issues or PR。html
Tornado中推薦使用協程寫異步代碼. 協程使用了Python的yield
關鍵字代替鏈式回調來將程序掛起和恢復執行(像在 gevent中出現的輕量級線程合做方式有時也被稱爲協程,可是在Tornado中全部的協程使用明確的上下文切換,並被稱爲異步函數).python
使用協程幾乎像寫同步代碼同樣簡單, 而且不須要浪費額外的線程. 它們還經過減小上下文切換來 使併發編程更簡單.git
例子:github
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
async
和await
Python 3.5引入了async
和await
關鍵字(使用這些關鍵字的函數也被稱爲"原生協程").從Tornado 4.3,你能夠用它們代替yield
爲基礎的協程.只須要簡單的使用async def foo()
在函數定義的時候代替@gen.coroutine
裝飾器,用await
代替yield. 本文檔的其餘部分會繼續使用yield
的風格來和舊版本的Python兼容, 可是若是async
和await
可用的話,它們運行起來會更快:編程
async def fetch_coroutine(url): http_client = AsyncHTTPClient() response = await http_client.fetch(url) return response.body
await
關鍵字比yield
關鍵字功能要少一些.例如,在一個使用 yield
的協程中,你能夠獲得Futures
列表, 可是在原生協程中,你必須把列表用 tornado.gen.multi
包起來. 你也可使用 tornado.gen.convert_yielded
來把任何使用yield
工做的代碼轉換成使用await
的形式.網絡
雖然原生協程沒有明顯依賴於特定框架(例如它們沒有使用裝飾器,例如tornado.gen.coroutine
或asyncio.coroutine
), 不是全部的協程都和其餘的兼容. 有一個coroutine runner在第一個協程被調用的時候進行選擇, 而後被全部用await
直接調用的協程共享. Tornado的協程執行者(coroutine runner)在設計上是多用途的,能夠接受任何來自其餘框架的awaitable對象;其餘的協程運行時可能有不少限制(例如,asyncio
協程執行者不接受來自其餘框架的協程).基於這些緣由,咱們推薦組合了多個框架的應用都使用Tornado的協程執行者來進行協程調度.爲了能使用Tornado來調度執行asyncio的協程, 可使用tornado.platform.asyncio.to_asyncio_future
適配器.併發
包含了yield
關鍵字的函數是一個生成器(generator).全部的生成器都是異步的;當調用它們的時候,會返回一個生成器對象,而不是一個執行完的結果.@gen.coroutine
裝飾器經過yield
表達式和生成器進行交流, 並且經過返回一個.Future
與協程的調用方進行交互.框架
下面是一個協程裝飾器內部循環的簡單版本:異步
# tornado.gen.Runner 簡化的內部循環 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
對象執行完成, 而後"解開(unwraps)"這個Future
對象,並把結果做爲yield
表達式的結果傳回給生成器.大多數異步代碼歷來不會直接接觸Future
類.除非 Future
當即經過異步函數返回給yield
表達式.async
協程通常不會拋出異常: 它們拋出的任何異常將被.Future
捕獲直到它被獲得.這意味着用正確的方式調用協程是重要的, 不然你可能有被忽略的錯誤:
@gen.coroutine def divide(x, y): return x / y def bad_call(): # 這裏應該拋出一個 ZeroDivisionError 的異常, 但事實上並無 # 由於協程的調用方式是錯誤的. divide(1, 0)
幾乎全部的狀況下, 任何一個調用協程的函數都必須是協程它自身, 而且在調用的時候使用yield
關鍵字. 當你複寫超類中的方法, 請參閱文檔,看看協程是否支持(文檔應該會寫該方法"多是一個協程"或者"可能返回一個 Future
類 "):
@gen.coroutine def good_call(): # yield 將會解開 divide() 返回的 Future 而且拋出異常 yield divide(1, 0)
有時你可能想要對一個協程"一勞永逸"並且不等待它的結果. 在這種狀況下,建議使用.IOLoop.spawn_callback
, 它使得.IOLoop
負責調用. 若是它失敗了, .IOLoop
會在日誌中把調用棧記錄下來:
# IOLoop 將會捕獲異常,而且在日誌中打印棧記錄. # 注意這不像是一個正常的調用, 由於咱們是經過 # IOLoop 調用的這個函數. IOLoop.current().spawn_callback(divide, 1, 0)
最後, 在程序頂層, 若是.IOLoop
還沒有運行, 你能夠啓動.IOLoop
,執行協程,而後使用.IOLoop.run_sync
方法中止.IOLoop
. 這一般被用來啓動面向批處理程序的main
函數:
# run_sync() 不接收參數,因此咱們必須把調用包在lambda函數中. IOLoop.current().run_sync(lambda: divide(1, 0))
爲了使用回調代替.Future
與異步代碼進行交互, 把調用包在.Task
類中. 這將爲你添加一個回調參數而且返回一個能夠yield的.Future
:
@gen.coroutine def call_task(): # 注意這裏沒有傳進來some_function. # 這裏會被Task翻譯成 # some_function(other_args, callback=callback) yield gen.Task(some_function, other_args)
從協程調用阻塞函數最簡單的方式是使用concurrent.futures.ThreadPoolExecutor
, 它將返回和協程兼容的Futures
:
thread_pool = ThreadPoolExecutor(4) @gen.coroutine def call_blocking(): yield thread_pool.submit(blocking_func, args)
協程裝飾器能識別列表或者字典對象中各自的 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] # 響應是和HTTPResponses相同順序的列表 @gen.coroutine def parallel_fetch_dict(urls): responses = yield {url: http_client.fetch(url) for url in urls} # 響應是一個字典 {url: HTTPResponse}
有時候保存一個 .Future
比當即yield它更有用, 因此你能夠在等待以前
執行其餘操做:
@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()
協程的循環是棘手的, 由於在Python中沒有辦法在for
循環或者while
循環yield
迭代器,而且捕獲yield的結果. 相反,你須要將循環條件從訪問結果中分離出來, 下面是一個使用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()
PeriodicCallback
一般不使用協程. 相反,一個協程能夠包含一個while True:
循環並使用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)
有時可能會遇到一個更復雜的循環. 例如, 上一個循環運行每次花費60+N
秒,其中N
是do_something()
花費的時間. 爲了準確的每60秒運行,使用上面的交叉模式:
@gen.coroutine def minute_loop2(): while True: nxt = gen.sleep(60) # 開始計時. yield do_something() # 計時後運行. yield nxt # 等待計時結束.