非同步與非阻塞 I/O¶
即時網路功能需要每個使用者都有一個長時間存活且大部分時間處於閒置狀態的連線。在傳統的同步網頁伺服器中,這意味著為每個使用者分配一個執行緒,這可能會非常耗費資源。
為了盡量減少並行連線的成本,Tornado 使用單執行緒的事件迴圈。這意味著所有應用程式碼都應以非同步且非阻塞的方式為目標,因為一次只能有一個操作處於活動狀態。
術語「非同步」和「非阻塞」密切相關,並且經常可以互換使用,但它們並非完全相同。
阻塞 (Blocking)¶
當函式等待某事發生後才返回時,該函式會被阻塞。函式可能會因多種原因而阻塞:網路 I/O、磁碟 I/O、互斥鎖等等。事實上,每個函式都會阻塞,至少在執行和使用 CPU 時會阻塞一點時間(舉一個極端的例子,說明為什麼 CPU 阻塞必須與其他類型的阻塞一樣認真對待,請考慮像 bcrypt 這樣的密碼雜湊函式,它設計上會使用數百毫秒的 CPU 時間,遠遠超過典型的網路或磁碟存取)。
函式在某些方面可能是阻塞的,而在其他方面則是非阻塞的。在 Tornado 的上下文中,我們通常談論的是網路 I/O 的阻塞,儘管所有類型的阻塞都應該盡量減少。
非同步 (Asynchronous)¶
非同步函式會在完成之前返回,並且通常會導致一些工作在背景中發生,然後在應用程式中觸發一些未來的動作(與普通的同步函式相反,同步函式會在返回之前完成它們要做的一切)。非同步介面有很多種風格:
回呼引數
返回一個佔位符 (
Future
,Promise
,Deferred
)傳遞到佇列
回呼註冊 (例如 POSIX 信號)
無論使用哪種類型的介面,非同步函式根據定義會以不同的方式與其呼叫者互動;沒有辦法讓同步函式以對其呼叫者透明的方式變為非同步(像 gevent 這樣的系統使用輕量級執行緒來提供與非同步系統相當的效能,但它們實際上並沒有使事情非同步化)。
Tornado 中的非同步操作通常會返回佔位符物件 (Futures
),除了像 IOLoop
這樣使用回呼的一些底層元件。 Futures
通常使用 await
或 yield
關鍵字轉換為其結果。
範例¶
這是一個同步函式的範例
from tornado.httpclient import HTTPClient
def synchronous_fetch(url):
http_client = HTTPClient()
response = http_client.fetch(url)
return response.body
這是將相同的函式以非同步方式重寫為原生協程
from tornado.httpclient import AsyncHTTPClient
async def asynchronous_fetch(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body
或者,為了與較舊版本的 Python 相容,可以使用 tornado.gen
模組
from tornado.httpclient import AsyncHTTPClient
from tornado import gen
@gen.coroutine
def async_fetch_gen(url):
http_client = AsyncHTTPClient()
response = yield http_client.fetch(url)
raise gen.Return(response.body)
協程有點神奇,但它們在內部所做的事情類似於這樣
from tornado.concurrent import Future
def async_fetch_manual(url):
http_client = AsyncHTTPClient()
my_future = Future()
fetch_future = http_client.fetch(url)
def on_fetch(f):
my_future.set_result(f.result().body)
fetch_future.add_done_callback(on_fetch)
return my_future
請注意,協程在其擷取完成之前就返回其 Future
。這就是使協程成為非同步的原因。
您可以使用回呼物件來執行任何可以使用協程執行的操作,但協程提供了一個重要的簡化,讓您可以像同步程式碼一樣組織程式碼。這對於錯誤處理尤其重要,因為 try
/except
區塊在協程中的工作方式與您的預期相同,而這很難透過回呼來實現。協程將在本指南的下一節中深入討論。