異步、非阻塞與協程

異步

異步函數會在完成之前返回,在應用中觸發下一個動作之前通常會在後 臺執行一些工作(和正常的 同步 函數在返回前就執行完所有的事情不同).這裏列 舉了幾種風格的異步接口:

回調參數
返回一個佔位符 (Future, Promise, Deferred)
傳送給一個隊列
回調註冊表 (POSIX信號)

不論使用哪種類型的接口, 按照定義 異步函數與它們的調用者都有着不同的交互方 式;也沒有什麼對調用者透明的方式使得同步函數異步(類似 gevent 使用輕量級線程的系統性能雖然堪比異步系統,但它們並 沒有真正的讓事情異步).

阻塞

一個函數在等待某些事情的返回值的時候會被 阻塞. 函數被阻塞的原因有很多: 網絡I/O,磁盤I/O,互斥鎖等.事實上 每個 函數在運行和使用CPU的時候都或多或少 會被阻塞(舉個極端的例子來說明爲什麼對待CPU阻塞要和對待一般阻塞一樣的嚴肅: 比如密碼哈希函數 bcrypt, 需要消耗幾百毫秒的CPU時間,這已 經遠遠超過了一般的網絡或者磁盤請求時間了).

一個函數可以在某些方面阻塞在另外一些方面不阻塞.例如, tornado.httpclient 在默認的配置下,會在DNS解析上面阻塞,但是在其他網絡請 求的時候不阻塞 (爲了減輕這種影響,可以用 ThreadedResolver 或者是 通過正確配置 libcurltornado.curl_httpclient 來做). 在Tornado的上下文中,我們一般討論網絡I/O上下文的阻塞,儘管各種阻塞已經被最小 化.

協程

Tornado中推薦使用 協程 寫異步代碼. 協程使用了Python的 yield 關鍵字代替鏈式回調來將程序掛起和恢復執行(像在 gevent 中出現的輕量級線程合作方式有時也被稱爲協程, 但是在Tornado中所有的協程使用明確的上下文切換,並被稱爲異步函數).
使用協程幾乎像寫同步代碼一樣簡單, 並且不需要浪費額外的線程. 它們還通過減少上下文切換來 使併發編程更簡單 .
Python 3.5 引入了 asyncawait 關鍵字(使用這些關鍵字的 函數也被稱爲”原生協程”). 從Tornado 4.3, 你可以用它們代替 yield 爲基礎的協程. 只需要簡單的使用 async def foo() 在函數定義的時候代替 @gen.coroutine 裝飾器, 用 await 代替yield. 本文檔的其他部分會繼續使用 yield 的風格來和舊版本的Python兼容, 但是如果 asyncawait 可用的話,它們運行起來會更快:

async and await

await 關鍵字比 yield 關鍵字功能要少一些. 例如,在一個使用 yield 的協程中, 你可以得到 Futures 列表, 但是在原生協程中,你必須把列表用 tornado.gen.multi 包起來. 你也可以使用 tornado.gen.convert_yielded 來把任何使用 yield 工作的代碼轉換成使用 await 的形式.
雖然原生協程沒有明顯依賴於特定框架(例如它們沒有使用裝飾器,例如 tornado.gen.coroutineasyncio.coroutine, 不是所有的協程都和其他的兼容. 有一個 協程執行者coroutine runner)在第一個協程被調用的時候進行選擇, 然後被所有用 await 直接調用的協程共享. Tornado 的協程執行者(coroutine runner)在設計上是多用途的,可以接受任何來自其他框架的awaitable對象; 其他的協程運行時可能有很多限制(例如, asyncio 協程執行者不接受來自其他框架的協程). 基於這些原因,我們推薦組合了多個框架的應用都使用Tornado的協程執行者來進行協程調度. 爲了能使用Tornado來調度執行asyncio的協程, 可以使用 tornado.platform.asyncio.to_asyncio_future 適配器.

包含了 yield 關鍵字的函數是一個 生成器. 所有的生成器都是異步的; 當調用它們的時候,會返回一個生成器對象,而不是一個執行完的結果. @gen.coroutine 裝飾器通過 yield 表達式和生成器進行交流, 而且通過返回一個 Future 與協程的調用方進行交互.

裝飾器從生成器接收一個 Future 對象, 等待(非阻塞的)這個 Future 對象執行完成, 然後”解開” 這個 Future 對象,並把結果作爲 yield 表達式的結果傳回給生成器. 大多數異步代碼從來不會直接接觸 Future 類 除非 Future 立即通過異步函數返回給 yield 表達式

如何調用協程

協程一般不會拋出異常: 它們拋出的任何異常將被 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))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章