為何你還不懂得如何使用Python協程

GoT陽仔發表於2019-05-05

在前一篇《一文徹底搞懂Python可迭代(Iterable)、迭代器(Iterator)和生成器(Generator)的概念》 的文中,知道生成器(Generator)可由以下兩種方式定義:

  • 列表生成器
  • 使用yield定義的函式

Python早期的版本中協程也是通過生成器來實現的,也就是基於生成器的協程(Generator-based Coroutines)。在前一篇介紹生成器的文章末尾舉了一個生產者-消費者的例子,就是基於生成器的協程來實現的。

def producer(c):
    n = 0
    while n < 5:
        n += 1
        print('producer {}'.format(n))
        r = c.send(n)
        print('consumer return {}'.format(r))


def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('consumer {} '.format(n))
        r = 'ok'


if __name__ == '__main__':
    c = consumer()
    next(c)  # 啟動consumer
    producer(c)
複製程式碼

看了這段程式碼,相信很多初學者和我一樣對基於生成器的協程實現其實很難馬上就能夠根據業務寫出自己的協程程式碼。Python實現者們也注意到這個問題,因為它太不Pythonic了。而基於生成器的協程也將被廢棄,因此本文將重點介紹asyncio包的使用,以及涉及到的一些相關類概念。
注:我使用的Python環境是3.7。

0x00 何為協程(Coroutine)

協程(Coroutine)是線上程中執行的,可理解為微執行緒,但協程的切換沒有上下文的消耗,它比執行緒更加輕量些。一個協程可以隨時中斷自己讓另一個協程開始執行,也可以從中斷處恢復並繼續執行,它們之間的排程是由程式設計師來控制的(可以看本文開篇處生產者-消費者的程式碼)。

定義一個協程

Python3.5+版本新增了aysncawait關鍵字,這兩個語法糖讓我們非常方便地定義和使用協程。
在函式定義時用async宣告就定義了一個協程。

import asyncio

# 定義了一個簡單的協程
async def simple_async():
    print('hello')
    await asyncio.sleep(1) # 休眠1秒
    print('python')
    
# 使用asynio中run方法執行一個協程
asyncio.run(simple_async())

# 執行結果為
# hello
# python
複製程式碼

在協程中如果要呼叫另一個協程就使用await要注意await關鍵字要在async定義的函式中使用,而反過來async函式可以不出現await

# 定義了一個簡單的協程
async def simple_async():
    print('hello')
    
asyncio.run(simple_async())

# 執行結果
# hello
複製程式碼

asyncio.run()將執行傳入的協程,負責管理asyncio事件迴圈。
除了run()方法可直接執行協程外,還可以使用事件迴圈loop

async def do_something(index):
    print(f'start {time.strftime("%X")}', index)
    await asyncio.sleep(1)
    print(f'finished at {time.strftime("%X")}', index)


def test_do_something():
    # 生成器產生多個協程物件
    task = [do_something(i) for i in range(5)]

    # 獲取一個事件迴圈物件
    loop = asyncio.get_event_loop()
    # 在事件迴圈中執行task列表
    loop.run_until_complete(asyncio.wait(task))
    loop.close()

test_do_something()

# 執行結果
# start 00:04:03 3
# start 00:04:03 4
# start 00:04:03 1
# start 00:04:03 2
# start 00:04:03 0
# finished at 00:04:04 3
# finished at 00:04:04 4
# finished at 00:04:04 1
# finished at 00:04:04 2
# finished at 00:04:04 0
複製程式碼

可以看出幾乎同時啟動了所有的協程。
其實翻閱原始碼可知asyncio.run()的實現也是封裝了loop物件及其呼叫。而asyncio.run()每次都會建立一個新的事件迴圈物件用於執行協程。

0x01 Awaitable物件

Python中可等待(Awaitable)物件有:協程(corountine)、任務(Task)、Future。即這些物件可以使用await關鍵字進行呼叫

await awaitable_object
複製程式碼
1. 協程(Coroutine)

協程由async def宣告定義,一個協程可由另一個協程使用await進行呼叫

async def nested():
    print('in nested func')
    return 13


async def outer():

    # 要使用await 關鍵字 才會執行一個協程函式返回的協程物件
    print(await nested())

asyncio.run(outer())

# 執行結果
# in nested func
# 13
複製程式碼

如果在outer()方法中直接呼叫nested()而不使用await,將丟擲一個RuntimeWarning

async def outer():
    # 直接呼叫協程函式不會發生執行,只是返回一個 coroutine 物件
    nested()
    
asyncio.run(outer())
複製程式碼

執行程式,控制檯將輸出以下資訊

RuntimeWarning: coroutine 'nested' was never awaited
  nested()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
複製程式碼
2. 任務(Task)

任務(Task)是可以用來併發地執行協程。可以使用asyncio.create_task()將一個協程物件封裝成任務,該任務將很快被排入排程佇列並執行。

async def nested():
    print('in nested func')
    return 13

async def create_task():
    # create_task 將一個協程物件打包成一個 任務時,該協程就會被自動排程執行
    task = asyncio.create_task(nested())
    # 如果要看到task的執行結果
    # 可以使用await等待協程執行完成,並返回結果
    ret = await task
    print(f'nested return {ret}')

asyncio.run(create_task())

# 執行結果
# in nested func
# nested return 13
複製程式碼

注:關於併發下文還會詳細說明。

3. Future

Future是一種特殊的低層級(low-level)物件,它是非同步操作的最終結果(eventual result)。 當一個 Future 物件 被等待,這意味著協程將保持等待直到該 Future 物件在其他地方操作完畢。

通常在應用層程式碼不會直接建立Future物件。在某些庫和asyncio模組中的會使用到該物件。

async def used_future_func():
    await function_that_returns_a_future_object()
複製程式碼

0x02 併發

1. Task

前面我們知道Task可以併發地執行。 asyncio.create_task()就是一個把協程封裝成Task的方法。

async def do_after(what, delay):
    await asyncio.sleep(delay)
    print(what)

# 利用asyncio.create_task建立並行任務
async def corun():
    task1 = asyncio.create_task(do_after('hello', 1)) # 模擬執行1秒的任務
    task2 = asyncio.create_task(do_after('python', 2)) # 模擬執行2秒的任務

    print(f'started at {time.strftime("%X")}')
    # 等待兩個任務都完成,兩個任務是並行的,所以總時間兩個任務中最大的執行時間
    await task1
    await task2

    print(f'finished at {time.strftime("%X")}')

asyncio.run(corun())

# 執行結果
# started at 23:41:08
# hello
# python
# finished at 23:41:10
複製程式碼

task1是一個執行1秒的任務,task2是一個執行2秒的任務,兩個任務併發的執行,總共消耗2秒。

2. gather

除了使用asyncio.create_task()外還可以使用asyncio.gather(),這個方法接收協程引數列表

async def do_after(what, delay):
    await asyncio.sleep(delay)
    print(what)
    
async def gather():
    print(f'started at {time.strftime("%X")}')
    # 使用gather可將多個協程傳入
    await asyncio.gather(
        do_after('hello', 1),
        do_after('python', 2),
    )
    print(f'finished at {time.strftime("%X")}')

asyncio.run(gather())

# 執行結果
# started at 23:47:50
# hello
# python
# finished at 23:47:52
複製程式碼

兩個任務消耗的時間為其中消耗時間最長的任務。

0x03 引用

  1. docs.python.org/3/library/a…

相關文章