最新Tornado5.11官方文檔翻譯(3)-用戶手冊-協程

導航

協程

Coroutines是在Tornado中編寫異步代碼的推薦方法。Coroutines使用Pythonawaityield關鍵字來掛起和恢復執行而不是一系列回調(在gevent這樣的框架中看到的協作輕量級線程有時也被稱爲協程,但在Tornado中所有協程都使用顯式上下文切換並被稱爲異步函數)。

協程幾乎和同步代碼一樣簡單,而且沒有線程那樣的昂貴開銷。它們還通過減少可能發生的上下文切換來簡化併發。

例子:

async def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

原生協程VS裝飾器協程

Python 3.5引入了async和await關鍵字(使用這些關鍵字的函數也稱爲“native coroutines”)。 爲了與舊版本的Python兼容,您可以使用tornado.gen.coroutine裝飾器來使用“decorated”或“yield-based”的協程。

儘可能使用原生協程。 僅在需要與舊版本的Python兼容時才使用裝飾器協程。Tornado文檔中的示例通常使用原生形式。

兩種形式之間的轉換通常很簡單:

# Decorated:                    # Native:

# Normal function declaration
# with decorator                # "async def" keywords
@gen.coroutine
def a():                        async def a():
    # "yield" all async funcs       # "await" all async funcs
    b = yield c()                   b = await c()
    # "return" and "yield"
    # cannot be mixed in
    # Python 2, so raise a
    # special exception.            # Return normally
    raise gen.Return(b)             return b

其它兩種形式的協程區別:

  • 原生協程通常更快。
  • 原生協程可以使用async forasync語句,這使得某些模式更加簡單。
  • 除非yieldawait它們,否則原生協程根本不會運行。裝飾器協程一旦被調用就可以“在後臺”開始運行。請注意,對於這兩種協程,使用awaityield很重要,這樣任何異常才能正常拋出。
  • 裝飾器協程與concurrent.futures包有額外的集成,允許直接生成executor.submi的結果。對於原生協程,請改用IOLoop.run_in_executor
  • 裝飾器協程通過產生列表或字典來支持等待多個對象的一些簡寫。使用tornado.gen.multi在原生協程中執行此操作。
  • 裝飾器協程可以支持與其他軟件包的集成,包括通過轉換函數註冊表的Twisted。要在原生協程中訪問此功能,請使用tornado.gen.convert_yielded
  • 裝飾器協程總是返回一個Future對象。原生協程返回一個不是Future的等待對象。在Tornado中,兩者大多可以互換。

工作原理

本節介紹裝飾器協程的操作。原生協程在概念上是相似的,但由於與Python運行時的額外集成而稍微複雜一些。

包含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完成,然後“展開”Future並將結果作爲yield表達式的結果發送回生成器。 大多數異步代碼從不直接接觸Future類,除非立即將異步函數返回的Future傳遞給yield表達式。

如何調用一個協程

協程不會以正常方式拋出異常:它們拋出的任何異常都將被困在等待對象中,直到它被放棄爲止。 這意味着以正確的方式調用協同程序很重要,否則您可能會發現未被注意到的錯誤:

async 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.
    divide(1, 0)

在幾乎所有情況下,任何調用協程的函數都必須是協程本身,並在調用中使用awaityield關鍵字。 當重寫超類中定義的方法時,請查閱文檔以查看是否允許協程(文檔應該說方法“可能是協程”或“可能返回Future”):

async def good_call():
    # await will unwrap the object returned by divide() and raise
    # the exception.
    await divide(1, 0)

有時你可能想要“Fire and forget”一個協程而不等待它的結果。在這種情況下,建議使用IOLoop.spawn_callback,這使得IOLoop負責調用。 如果失敗,IOLoop將記錄堆棧路徑:

# The IOLoop will catch the exception and print a stack trace in
# the logs. Note that this doesn't look like a normal call, since
# we pass the function object to be called by the IOLoop.
IOLoop.current().spawn_callback(divide, 1, 0)

對於使用@gen.coroutine的函數,建議以這種方式使用IOLoop.spawn_callback,但是使用async def的函數需要它(否則協程運行程序將無法啓動)。

最後,在程序的頂層,如果IOLoop尚未運行,您可以啓動IOLoop,運行協程,然後使用IOLoop.run_sync方法停止IOLoop。 這通常用於啓動面向批處理( batch-oriented)程序的main函數:

# run_sync() doesn't take arguments, so we must wrap the
# call in a lambda.
IOLoop.current().run_sync(lambda: divide(1, 0))

協程模式

調用阻塞函數(Calling blocking functions)

從協程中調用一個阻塞函數的最簡單的方法就是使用ThreadPoolExecutor,返回一個其他協程兼容的Futures對象:

async def call_blocking():
    await IOLoop.current().run_in_executor(None, blocking_func, args)
並行(Parallelism)

multi函數接受其值爲Futures的列表和dicts,並且並行等待所有這些Futures

from tornado.gen import multi

async def parallel_fetch(url1, url2):
    resp1, resp2 = await multi([http_client.fetch(url1),
                                http_client.fetch(url2)])

async def parallel_fetch_many(urls):
    responses = await multi ([http_client.fetch(url) for url in urls])
    # responses is a list of HTTPResponses in the same order

async def parallel_fetch_dict(urls):
    responses = await multi({url: http_client.fetch(url)
                             for url in urls})
    # responses is a dict {url: HTTPResponse}

在裝飾器協程中,可以直接yield列表或字典:

@gen.coroutine
def parallel_fetch_decorated(url1, url2):
    resp1, resp2 = yield [http_client.fetch(url1),
                          http_client.fetch(url2)]
交叉存取(Interleaving)

有時保存一個Future對象比立即yield它會更有用,以便你可以在等待之前開始開始另一個操作:

from tornado.gen import convert_yielded

async def get(self):
    # convert_yielded() starts the native coroutine in the background.
    # This is equivalent to asyncio.ensure_future() (both work in Tornado).
    fetch_future = convert_yielded(self.fetch_next_chunk())
    while True:
        chunk = yield fetch_future
        if chunk is None: break
        self.write(chunk)
        fetch_future = convert_yielded(self.fetch_next_chunk())
        yield self.flush()

這對於裝飾的協同程序來說更容易一些,因爲它們在被調用時立即啓動:

@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()
循環(Looping)

在原生協程中,可以使用async for。在舊版本的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()
後臺運行(Running in the background)

PeriodicCallback通常不與協同程序一起使用。相反,一個協程可以包含一個while True::循環並使用tornado.gen.sleep

async def minute_loop():
    while True:
        await do_something()
        await gen.sleep(60)

# Coroutines that loop forever are generally started with
# spawn_callback().
IOLoop.current().spawn_callback(minute_loop)

有時可能需要更復雜的循環。 例如,前一個循環每60 + N秒運行一次,其中Ndo_something()的運行時間。 要完全每60秒運行一次,請使用上面的交叉存取:

async def minute_loop2():
    while True:
        nxt = gen.sleep(60)   # Start the clock.
        await do_something()  # Run while the clock is ticking.
        await nxt             # Wait for the timer to run out.

上一篇:異步與非阻塞I/O

下一篇:Queue示例 - 一個併發的網絡爬蟲

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章