深入理解協程(三):async/await實現異步協程


原創不易,轉載請聯系作者

深入理解協程分為三部分進行講解:

  • 協程的引入
  • yield from實現異步協程
  • async/await實現異步協程

本篇為深入理解協程系列文章的最后一篇

從本篇你將了解到:

  1. async/await的使用。
  2. 如何從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/awaityield from風格的協程底層實現方式相同。因此,從yield from風格改為async/await風格非常容易。只需:

  • @asyncio.coroutine替換為async
  • yield from替換為await

async/await風格的代碼隱藏了裝飾器、yield from語法,方便了人們的理解,同時也讓代碼更加簡潔。

推薦閱讀

深入理解協程(一):協程的引入

深入理解協程(二):yield from實現異步協程

年底送福利,關注公眾號【西加加先生】,2020一起玩Python,公眾號回復【2020】即可獲取抽獎方式參與抽獎
在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM