2.4、User’s guide (Coroutines)

weixin_33890499發表於2018-03-29
Coroutines
協程在 tornado 的非同步程式碼中是被推薦使用的。協程使用 python 的 yield 關鍵字去暫停和恢復執行,來替代回撥鏈。(合作輕量級執行緒如 gevent 有時也被叫做協程,但是在 tornado 中,所有的協程使用顯示的上下文切換被稱為非同步函式。)
協程和同步程式碼一樣簡單,但是不用花費一個執行緒的代價。通過減少上下文切換髮生地方的數量,使併發更容易思考。

Example

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    return response.body
Python 3.5:async and wait
python 3.5 介紹了 async 和 await 關鍵字(使用這兩個關鍵字的也被稱為原生協程)。

從 tornado 4.3 版本,你也可以使用它們替代絕大多數的 yield-based 協程。簡單的使用 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
yield 關鍵字比 await 更通用;

例如在一個 yield-based 的協程,可以 yield 一個 Futures 列表,但是在原生的協程,你必須用 tornado.gen.multi 將他們包裝起來。這也消除了同 concurrent.futures 的整合。

你可以使用 tornado.gen.convert_yielded 將 yield 的用法轉換為 await 進行使用。
async def f():
    executor = concurrent.futures.ThreadPoolExecutor()
    await tornado.gen.convert_yielded(executor.submit(g))

How it works

一個函式包含了 yield,那麼這個函式就是一個生成器。所有的生成器都是非同步的,當呼叫時,他們返回一個生成器物件而不是執行完成。

@gen.coroutine 裝飾器和生成器通過 yield 表示式,給協程的呼叫者返回一個 Future。
  • 簡單版本的協程內部迴圈
# Simplified inner loop of 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 執行完成,然後將結果作為 yield 表示式傳送給生成器。

大多數非同步程式碼從未直接接觸 Future 類,除了通過非同步函式的 yield 表示式獲取的 Future。

How to call a coroutine

協程不是通過正常的方式報出異常,他們提出的任何異常直到他被 yielded 將會被包裝在 Future 裡。

這意味著需要使用正確的方式去呼叫協程,否則可能忽略異常錯誤。
@gen.coroutine
def divide(x, y):
    return x / y

def bad_call():
    # This should raise a ZeroDivisionError, but it won't because
    # the coroutine is called incorrectly.
    # 這裡應該要報 ZeroDivisionError 錯誤
    divide(1, 0)
幾乎所有的情況下,任何呼叫協程的函式本身也是協程,並且使用 yield 呼叫。

當你覆蓋一個父類中定義的方法時,查閱文件確認協程是否被允許(文件應該說這個方法可能是一個協程或可能返回一個 Future)
@gen.coroutine
def good_call():
    # yield will unwrap the Future returned by divide() and raise
    # the exception.
    yield divide(1, 0)
有時候你想執行後放任不管一個協程(不等待它的執行結果),這種情況下,建議使用 IOLoop.spawn_callback,使 IOLoop 負責呼叫。

如果執行失敗了,會將日誌堆疊跟蹤。
IOLoop.current().spawn_callback(divide, 1, 0)
使用 @gen.coroutine 的函式推薦使用 IOLoop.spawn_callback。

使用 async 的函式只能使用 IOLoop.spawn_callback (否則協程不會執行)
最後,在程式的頂層,如果 IOLoop 還沒有執行,你可以通過 IOLoop.run_sync 開始 IOLoop,執行協程,然後停止 IOLoop 。通常用於啟動一個面向批處理程式的主函式。
# run_sync() doesn't take arguments, so we must wrap the
# call in a lambda.
IOLoop.current().run_sync(lambda: divide(1, 0))

Coroutine patterns

  • Calling blocking functions
在協程裡面呼叫阻塞函式最簡單的方式是使用 IOLoop.run_in_executor,返回值是 Futures 相容協程。
@gen.coroutine
def call_blocking():
    yield IOLoop.current().run_in_executor(blocking_func, args)
  • Parallelism
協程裝飾器可以識別 value 值是 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]
    # responses is a list of HTTPResponses in the same order

@gen.coroutine
def parallel_fetch_dict(urls):
    responses = yield {url: http_client.fetch(url)
                        for url in urls}
    # responses is a dict {url: HTTPResponse}
當使用 await 時,列表和字典必須使用 tornado.gen.multi 包裝
async def parallel_fetch(url1, url2):
    resp1, resp2 = await gen.multi([http_client.fetch(url1),
                                    http_client.fetch(url2)])
  • Interleaving
有時候儲存一個 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()
這種模式常在 @gen.coroutine 中使用。

如果 fetch_next_chunk() 是 async 函式,那麼上述寫法需變更為 fetch_future = tornado.gen.convert_yielded(self.fetch_next_chunk()) 來啟動後臺處理。
  • Looping
在原生協程中,async for 可以使用。

在舊版的 python 中,迴圈是棘手的,因為沒有辦法使用 yield 去迭代並且捕捉 for 或者 while 的迴圈的結果,你需要將迴圈條件拆解來獲取結果,舉例如下:
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()
  • Running in the background
PeriodCallback 通常不用在協程,相反的,協程可以使用 while Ture: 迴圈並且使用 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)   # Start the clock.
        yield do_something()  # Run while the clock is ticking.
        yield nxt             # Wait for the timer to run out.

上一篇: 2.3、User’s guide (Queue)
下一篇: 2.5、User’s guide (Structure of a Tornado web application)

相關文章