原創不易,轉載請聯系作者
深入理解協程分為三部分進行講解:
- 協程的引入
- yield from實現異步協程
- async/await實現異步協程
本篇為深入理解協程系列文章的最后一篇。
從本篇你將了解到:
async/await的使用。- 如何從
yield from風格的協程修改為async/await風格。
篇幅較長,請耐心閱讀。
async/await的引入
上篇【yield from實現異步協程】我們引入了asynico模塊,結合yield from實現異步協程。但語法不夠簡潔,其中涉及的生成器,裝飾器也讓人頭疼不已。
為了語法更加簡潔。於是,在Python3.5(PEP 492)中新增了async/await語法來實現異步協程。
async/await的使用
先介紹幾個概念:
- async/await :python3.5之后用於定義協程的關鍵字,async定義一個協程,await用於掛起阻塞的異步調用接口。
- event_loop :事件循環,程序開啟一個無限的循環,程序員會把一些函數注冊到事件循環上。當滿足事件發生的時候,調用相應的協程函數。
- coroutine :協程對象,指一個使用async關鍵字定義的函數,它的調用不會立即執行函數,而是會返回一個協程對象。協程對象需要注冊到事件循環,由事件循環調用。
- task :任務,是對協程進一步封裝,其中包含任務的各種狀態。
- future: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別
1.創建協程
在def前加上async的聲明,就完成了一個協程函數的定義。協程函數不能直接調用運行,需要將協程注冊到事件循環,並啟動事件循環才能使用。
import asyncio
async def fun(a): # 定義協程函數
print(a)
# 調用協程函數,生成一個協程對象,此時協程函數並未執行
coroutine = fun('hello world')
# 創建事件循環
loop = asyncio.get_event_loop()
# 將協程函數添加到事件循環,並啟動
loop.run_until_complete(coroutine)
# 輸出
hello word
2. 任務對象task
協程對象不能直接運行,在注冊事件循環的時候,其實是run_until_complete方法將協程包裝成為了一個任務(task)對象。我們也可以顯式實現它。
實現方式1:
使用create_task()創建task。
import asyncio
async def fun(a):
print(a)
return a
coroutine = fun('hello world')
loop = asyncio.get_event_loop()
# 使用create_task()創建task,並將coroutine對象轉化成task對象
task = loop.create_task(coroutine)
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')
print(f'result: {result}')
輸出結果:
task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>
從輸出結果能看到,創建task對象后,未將task添加到事件循環之前,狀態是pending;task對象執行完畢后,狀態是finished,並將參數a的值返回。
實現方式2:
使用asyncio 的 ensure_future() 方法,創建task。
import asyncio
async def fun(a):
print(a)
return a
coroutine = fun('hello world')
# 使用asyncio 的 ensure_future() 方法,創建task,並將coroutine對象轉化成task對象
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')
輸出結果:
task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>
通過ensure_future() 可以在loop未定義前創建task。實現效果與上面相同。
3.綁定回調函數
如果需要在task執行完畢后對結果進行處理,可以通過給task綁定回調函數完成,回調的最后一個參數是future對象(如task對象)。
import asyncio
async def fun(a):
print(a)
return a
def callback(task): # 回調函數,打印task的返回值
print(f'result: {task.result()}')
coroutine = fun('hello world')
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
task.add_done_callback(callback) #綁定回調函數
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')
輸出結果:
task: <Task pending coro=<fun() running at D:/test.py:3> cb=[callback() at D:/Study/Python/python_text/非項目/協程.py:7]>
hello world
result: hello world # 完成了返回值的打印
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>
4.多任務協程
如果我們需要執行多個任務時,我們可以定義一個任務列表,並將需要完成的協程任務都加進去。將原本的loop.run_until_complete(tasks)改為loop.run_until_complete(asyncio.wait(tasks))。
如果執行的是多個耗時的任務,如網絡請求、文件讀取等。此時就await就派上用場了,await可以針對耗時的操作進行掛起,就像生成器里的yield一樣,函數讓出控制權。協程遇到await,事件循環將會掛起該協程,執行別的協程,直到其他的協程也掛起或者執行完畢,再進行下一個協程的執行。
舉個例子:
import time
import asyncio
async def taskIO_1():
print('開始運行IO任務1...')
await asyncio.sleep(2)
print('IO任務1已完成,耗時2s')
return taskIO_1.__name__
async def taskIO_2():
print('開始運行IO任務2...')
await asyncio.sleep(3)
print('IO任務2已完成,耗時3s')
return taskIO_2.__name__
if __name__ == '__main__':
start = time.time()
loop = asyncio.get_event_loop()
tasks = [taskIO_1(), taskIO_2()]
loop.run_until_complete(asyncio.wait(tasks)) # 完成事件循環,直到最后一個任務結束
print('所有IO任務總耗時%.5f秒' % float(time.time()-start))
# 輸出
開始運行IO任務2...
開始運行IO任務1...
IO任務1已完成,耗時2s
IO任務2已完成,耗時3s
所有IO任務總耗時3.00251秒
可以看出,原本需要5秒,現在執行只需要3秒。
yield from轉async/await
上述代碼有沒有很眼熟。
其實,這段代碼正是【yield from實現異步協程】末尾,yield from結合asynico實現異步協程的代碼。只是將yielf from風格變為async/await風格。
由於async/await與yield from風格的協程底層實現方式相同。因此,從yield from風格改為async/await風格非常容易。只需:
- 把
@asyncio.coroutine替換為async; - 把
yield from替換為await。
async/await風格的代碼隱藏了裝飾器、yield from語法,方便了人們的理解,同時也讓代碼更加簡潔。
推薦閱讀
年底送福利,關注公眾號【西加加先生】,2020一起玩Python,公眾號回復【2020】即可獲取抽獎方式參與抽獎。

