04 Python 基礎教學05 Python 爬蟲教學10 所有文章

【Python教學】淺談 Coroutine 協程使用方法

python_coroutine_協程

本篇大綱:

  • 什麼是 Coroutine (協程 / 微線程)?
  • 使用 Python asyncio library 寫 Coroutine
    1. 一個簡單的範例
    2. 了解 async / await 語法糖
    3. 如何建立事件循環?
    4. 如何建立任務 Task?
    5. 如何同時運行多個 Tasks 任務?

一. 什麼是 Coroutine (協程 / 微線程)?

先來看看 Python官方對 Coroutine 的解釋:

Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points. They can be implemented with the async def statement. See also PEP 492 .

Glossary — Python 3.8.2 documentation

所謂 Coroutine 就是一個可以暫停將執行權讓給其他 Coroutine 或 Awaitables obj 的函數,等其執行完後再繼續執行,並可以多次的進行這樣的暫停與繼續。

Asyncio is a library to write concurrent code using the async/await syntax.
asyncio is used as a foundation for multiple Python asynchronous frameworks that provide high-performance network and web-servers, database connection libraries, distributed task queues, etc.

asyncio — Asynchronous I/O — Python 3.8.2 documentation

Native coroutine:使用 Python 3.5+ library 中 async / await 語法和 asyncio library 來寫 Coroutine 稱為 native coroutine。

Generator-based coroutine:用 @asyncio.coroutin 來裝飾函數來寫 Coroutine 的方法稱為 Generator-based coroutine,Generator-based coroutines 將在Python 3.10 中被移除。

▍延伸問題:
1. 如果要判斷一個函數是不是 Coroutine?可以使用 asyncio.iscoroutinefunction(func) 方法判別。
2. 如果要判斷一個函數返回的是不是 Coroutine 對象? 可以使用 asyncio.iscoroutine(obj) 方法判別。

二. 使用 Python asyncio library

1. 一個簡單的範例

▍使用須知:

(以下範例 requires Python 3.7+)

import asyncio
import time

now = lambda: time.time()

async def dosomething(num):
    print('第 {} 任務,第一步'.format(num))
    await asyncio.sleep(2)
    print(‘第 {} 任務,第二步’.format(num))

if __name__ == "__main__":
    start = now()
    tasks = [dosomething(i) for i in range(5)]
    asyncio.run(asyncio.wait(tasks))
    print('TIME: ', now() - start)

終端機輸出:

>>> 第 1 任務,第一步
>>> 第 0 任務,第一步
>>> 第 2 任務,第一步
>>> 第 3 任務,第一步
>>> 第 4 任務,第一步
>>> 第 1 任務,第二步
>>> 第 0 任務,第二步
>>> 第 2 任務,第二步
>>> 第 3 任務,第二步
>>> 第 4 任務,第二步
>>> TIME:  2.0060410499572754

可以看到運行 dosomething 這個函數 5 次,總共只花了兩秒鐘;如果不使用異步的話則需要花費 10 秒鐘才能運行完成。

接下來我們會介紹:

  • async / await
  • asyncio.run / asyncio.get_event_loop
  • asyncio.create_task / asyncio.ensure_future
  • asyncio.wait / asyncio.gather

2.了解 async / await 語法糖

async / await 是 Python 3.5+ 之後出現的語法糖,讓 Coroutine 與 Coroutine 之間的調度更加清楚。

簡單來說:

  • async:用來宣告 function 能夠有異步的功能
  • await:用來標記 Coroutine 切換暫停和繼續的點

▍使用 async 用來宣告一個 native Coroutine:

  • 注意:async def 函數內無法與 yield 或 yield from 共同使用,會引發SyntaxError 錯誤
  • 使用方法:將 async 加在 function 前面,如下範例
async def read_data(db):
    pass

▍使用 await 讓 Coroutine 掛起:

  • 注意: await 後面必須接一個 Coroutine 對象或是 awaitable 類型的對象
  • await 的目的是將控制權回傳給事件循環 (event loop) 並等待返回,而背後實現暫停掛起函數操作的是 yield
  • 使用方法:加在要等待的 function 前面,如下範例
async def read_data(db):
    data = await db.fetch(‘SELECT ...’)

▍延伸問題 – 什麼是 Awaitables 特性:
有三種主要類型:coroutines、 Tasks 、Futures
1. coroutines:一個 async def 函數就是 coroutine,也因為 awaitables 特性所以可以等待其他 coroutine。
2. tasks:tasks 是用來調度 coroutines,可通過 asyncio.create_task( ) 來打包 coroutines。
3. futures:futures 是一個異步操作 (asynchronous operation) 返回的結果

3. 如何建立事件循環?

Python 3.5+ 使用 asyncio.get_event_loop 先建立一個 event_loop,然後再將 Coroutine 放進 run_until_complete() 裡面,直到所有 Coroutine 運行結束。

import asyncio

async def hello_world(x):
    print(“hello_world x” + str(x))
    await asyncio.sleep(x)

loop = asyncio.get_event_loop()
loop.run_until_complete(hello_world(3))
loop.close()

▍Python 3.7 推出更簡潔的方法:

Python 3.7+ 之後將 loop 封裝,只需要使用 asyncio.run() 一行程式就結束,不用在建立 event_loop 結束時也不需要 loop.close,因為他都幫你做完了,有興趣可以參考:cpython/runners.py at 3.8 · python/cpython · GitHub

import asyncio

async def hello_world(x):
    print(“hello_world x” + str(x))
    await asyncio.sleep(x)

asyncio.run(hello_world(2))

4. 如何建立任務 Task?

▍建立任務方法有兩種

  • asyncio.create_task( ) :Python 3.7+ 以上可使用
  • asyncio.ensure_future( ):可讀性較差
# In Python 3.7+
task = asyncio.create_task(main())

# This works in all Python versions but is less readable
task = asyncio.ensure_future(main())

完整的範例:

import asyncio 
import time 

async def dosomething(num): 
    print(‘start{}’.format(num)) 
    await asyncio.sleep(num) 
    print(‘sleep{}’.format(num)) 

async def main(): 
    task1 = asyncio.create_task(dosomething(1)) 
    task2 = asyncio.create_task(dosomething(2)) 
    task3 = asyncio.create_task(dosomething(3)) 
    await task1 
    await task2 
    await task3 

if __name__ == “__main__”: 
    time_start = time.time() 
    asyncio.run(main()) 
    print(time.time() - time_start) 

>> start1 
>> start2 
>> start3 
>> sleep1 
>> sleep2 
>> sleep3 
>> 3.0052239894866943 

5. 如何同時運行多個 Tasks 任務?

使用 asyncio.gather( ) ,可同時放入多個 Coroutines 或 awaitable object 進入事件循環 (event loop),等 Coroutines 都結束後,並依序收集其回傳值。

asyncio.gather( *aws, loop=None, return_exceptions=False)

  1. *aws :可傳入多個 awaitable objects
  2. Loop:此參數將會在 Python version 3.10 移除
  3. return_exceptions:default 是 False,當發生 exception 時會立即中斷 task,如果設定為 True 則發生錯誤的訊息會與其他成功訊息一起回傳(如下範例,最終的 results 結果裡面包含了 ValueError() 結果)
import asyncio
import time

now = lambda: time.time()

async def dosomething(num):
    print(‘第 {} 任務,第一步’.format(num))
    await asyncio.sleep(2)
    print(‘第 {} 任務,第二步’.format(num))
    return ‘第 {} 任務完成’.format(num)

async def raise_error(num):
    raise ValueError
    print(‘這邊不會執行到’)

async def main():
    tasks = [dosomething(i) for I in range(5)]
    tasks1 = [raise_error(i) for I in range(5)]

    results = await asyncio.gather(*tasks, *tasks1, return_exceptions=True)
    print(results)


if __name__ == “__main__”:

    start = now()
    asyncio.run(main())
    print(‘TIME: ‘, now() - start)
>>> 第 0 任務,第一步
>>> 第 1 任務,第一步
>>> 第 2 任務,第一步
>>> 第 3 任務,第一步
>>> 第 4 任務,第一步
>>> 第 0 任務,第二步
>>> 第 1 任務,第二步
>>> 第 2 任務,第二步
>>> 第 3 任務,第二步
>>> 第 4 任務,第二步
>>> ['第 0 任務完成', '第 1 任務完成', '第 2 任務完成', '第 3 任務完成', '第 4 任務完成', ValueError(), ValueError(), ValueError(), ValueError(), ValueError()]
TIME:  2.0053980350494385

到目前為止你已經可以運行多個協程任務。

下一篇會介紹 Async IO Design Patterns:

  • Chaining Coroutines
  • Using a Queue

【Python教學】Async IO Design Patterns 範例程式
【實戰篇】 解析 Python 之父寫的 web crawler 異步爬蟲

最後~

▍注意:
Python Async IO 目前還不斷迅速發展中,本篇的用法是參考 Python 3.8.2 documentation 文件撰寫範例。

▍回顧本篇我們介紹了的內容:

  • 什麼是 Coroutine (協程 / 微線程)?
  • 使用 Python Asyncio library 寫 Coroutine
    1. 一個簡單的範例
    2. 了解 async / await 語法糖
    3. 如何建立事件循環?
    4. 如何建立任務 Task?
    5. 如何同時運行多個 Tasks 任務?

▍關於與 Concurrency Programming 相關其他文章,可以參考:

那麼有關於【Python教學】淺談 Coroutine 協程使用方法 的介紹就到這邊告一個段落囉!有任何問題可以在以下留言~

有關 Max行銷誌的最新文章,都會發佈在 Max 的 Facebook 粉絲專頁,如果想看最新更新,還請您按讚或是追蹤唷!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *