Tornado 4.3 文檔翻譯: 用戶指南-協程

譯者說

Tornado 4.3于2015年11月6日發布,該版本正式支持Python3.5async/await關鍵字,并且用舊版本CPython編譯Tornado同樣可以使用這兩個關鍵字,這無疑是一種進步。其次,這是最后一個支持Python2.6Python3.2的版本了,在后續的版本了會移除對它們的兼容。現在網絡上還沒有Tornado4.3的中文文檔,所以為了讓更多的朋友能接觸并學習到它,我開始了這個翻譯項目,希望感興趣的小伙伴可以一起參與翻譯,項目地址是tornado-zh on Github,翻譯好的文檔在Read the Docs上直接可以看到。歡迎Issues or PR。

協程

Tornado中推薦使用協程寫異步代碼. 協程使用了Python的yield關鍵字代替鏈式回調來將程序掛起和恢復執行(像在 gevent中出現的輕量級線程合作方式有時也被稱為協程,但是在Tornado中所有的協程使用明確的上下文切換,并被稱為異步函數).

使用協程幾乎像寫同步代碼一樣簡單, 并且不需要浪費額外的線程. 它們還通過減少上下文切換來 使并發編程更簡單.

例子:

    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

Python 3.5:asyncawait

Python 3.5 引入了asyncawait關鍵字(使用這些關鍵字的函數也被稱為"原生協程"). 從Tornado 4.3,你可以用它們代替yield為基礎的協程.只需要簡單的使用async def foo()在函數定義的時候代替@gen.coroutine裝飾器, 用await代替yield. 本文檔的其他部分會繼續使用yield的風格來和舊版本的Python兼容, 但是如果asyncawait可用的話,它們運行起來會更快:


    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.coroutineasyncio.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表達式.

如何調用協程

協程一般不會拋出異常: 它們拋出的任何異常將被.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))

協程模式

結合 callback

為了使用回調代替.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秒,其中Ndo_something()花費的時間. 為了準確的每60秒運行,使用上面的交叉模式:

    @gen.coroutine
    def minute_loop2():
        while True:
            nxt = gen.sleep(60)   # 開始計時.
            yield do_something()  # 計時后運行.
            yield nxt             # 等待計時結束.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容