在了解了 Python
並發編程的多線程和多進程之後,我們來了解一下基於 asyncio
的異步IO編程--協程
01
協程簡介
協程(Coroutine)又稱微線程、纖程,協程不是進程或線程,其執行過程類似於 Python
函數調用,Python
的 asyncio
模塊實現的異步IO編程框架中,協程是對使用 async
關鍵字定義的異步函數的調用;
一個進程包含多個線程,類似於一個人體組織有多種細胞在工作,同樣,一個程序可以包含多個協程。多個線程相對獨立,線程的切換受系統控制。同樣,多個協程也相對獨立,但是其切換由程序自己控制。
02
一個簡單例子
我們來使用一個簡單的例子了解協程,首先看看下面的代碼:
import time
def display(num):
time.sleep(1)
print(num)
for num in range(10):
display(num)
很容易看得懂,程序會輸出0到9的數字,每隔1秒中輸出一個數字,因此整個程序的執行需要大約10秒 時間。值得注意的是,因為沒有使用多線程或多進程(並發),程序中只有一個執行單元(只有一個線程在 執行),而 time.sleep(1)
的休眠操作會讓整個線程停滯1秒鐘,
對於上面的代碼來說,在這段時間裡面 CPU是閒置的沒有做什麼事情。
我們再來看看使用協程會發生什麼:
import asyncio
async def display(num): # 在函數前使用async關鍵字,變成異步函數 await asyncio.sleep(1)
print(num)
異步函數不同於普通函數,調用普通函數會得到返回值,而調用異步函數會得到一個協程對象。我們需要將協程對象放到一個事件循環中才能達到與其他協程對象協作的效果,因為事件循環會負責處理子程 序切換的操作。
簡單的說就是讓阻塞的子程序讓出CPU給可以執行的子程序。
03
基本概念
異步IO是指程序發起一個IO操作(阻塞等待)後,不用等IO操作結束,可以繼續其它操作;做其他事情,當IO操作結束時,會得到通知,然後繼續執行。異步IO編程是實現並發的一種方式,適用於IO密集型任務
Python
模塊 asyncio
提供了一個異步編程框架,全局的流程圖大致如下:
下面對每個函數都從代碼層面進行介紹
async
: 定義一個方法(函數),這個方法在後面的調用中不會被立即執行而是返回一個協程對象;
async def test(): print('hello 異步')
test() # 調用異步函數
輸出:RuntimeWarning: coroutine 'test' was never awaited
coroutine
: 協程對象,也可以將協程對象添加到時間循環中,它會被事件循環調用;
async def test():
print('hello 異步')
c = test() # 調用異步函數,得到協程對象-->c
print(c)
輸出:<coroutine object test at 0x0000023FD05AA360>
event_loop
: 事件循環,相當於一個無限循環,可以把一些函數添加到這個事件中,函數不會立即執行, 而是滿足某些條件的時候,函數就會被循環執行;
async def test():
print('hello 異步')
c = test() # 調用異步函數,得到協程對象-->c
loop = asyncio.get_event_loop() # 創建事件循環
loop.run_until_complete(c) # 把協程對象丟給循環,並執行異步函數內部代碼
輸出:hello 異步
await
: 用來掛起阻塞方法的執行;
import asyncio
def running1():
async def test1():
print('1')
await test2()
print('2')
async def test2():
print('3')
print('4')
loop = asyncio.get_event_loop()
loop.run_until_complete(test1())
if __name__ == '__main__':
running1()
輸出:
task
: 任務,對協程對象的進一步封裝,包含任務的各個狀態;
async def test():
print('hello 異步')
c = test() # 調用異步函數,得到協程對象-->c
loop = asyncio.get_event_loop() # 創建事件循環
task = loop.create_task(c) # 創建task任務
print(task)
loop.run_until_complete(task) # 執行任務
輸出:
<Task pending coro=<test() running at D: /xxxx.py>> # task
hello 異步 # 異步函數內部代碼一樣執行
future
: 代表以後執行或者沒有執行的任務,實際上和task
沒有本質區別;這裡就不做代碼展示;
首先使用一般方式方法創建一個函數:
def func(url):
print(f'正在對{url}發起請求:')
print(f'請求{url}成功!')
func('www.baidu.com')
結果如下所示:
正在對www.baidu.com發起請求:
請求www.baidu.com成功
04
基本操作
通過 async
關鍵字定義一個異步函數,調用異步函數返回一個協程對象。
異步函數就是在函數執行過程中掛起,去執行其他異步函數,等待掛起條件(time.sleep(n)
)消失後,再回來執行,接著我們來修改上述代碼:
async def func(url):
print(f'正在對{url}發起請求:')
print(f'請求{url}成功!')
func('www.baidu.com')
結果如下:
RuntimeWarning: coroutine 'func' was never awaited
這就是之前提到的,使用async關鍵字使得函數調用得到了一個協程對象,協程不能直接運行,需要把協程 加入到事件循環中,由後者在適當的時候調用協程;
task任務對象是對協程對象的進一步封裝;
import asyncio
async def func(url):
print(f'正在對{url}發起請求:')
print(f'請求{url}成功!')
c = func('www.baidu.com') # 函數調用的寫成對象--> c
loop = asyncio.get_event_loop() # 創建一個時間循環對象
task = loop.create_task(c)
loop.run_until_complete(task) # 注冊加啟動
print(task)
結果如下:
正在對www.baidu.com發起請求:
請求www.baidu.com成功!
<Task finished coro=<func() done, defined at D:/data_/test.py:10> result=None>
前面我們提及到future和task沒有本質區別
async def func(url):
print(f'正在對{url}發起請求:')
print(f'請求{url}成功!')
c = func('www.baidu.com') # 函數調用的寫成對象--> c
loop = asyncio.get_event_loop() # 創建一個時間循環對象
future_task = asyncio.ensure_future(c)
print(future_task,'未執行')
loop.run_until_complete(future_task) # 注冊加啟動
print(future_task,'執行完了')
結果如下:
<Task pending coro=<func() running at D:/data/test.py:10>>未執行
正在對www.baidu.com發起請求:
請求www.baidu.com成功!
<Task finished coro=<func() done, defined at D:/data/test.py:10> result=None> 執行完了
在異步函數中,可以使用await關鍵字,針對耗時的操作(例如網絡請求、文件讀取等IO操作)進行掛起,比如異步程序執行到某一步時需要很長時間的等待,就將此掛起,去執行其他異步函數
import asyncio, time
async def do_some_work(n): #使用async關鍵字定義異步函數
print('等待:{}秒'.format(n))
await asyncio.sleep(n) #休眠一段時間
return '{}秒後返回結束運行'.format(n)
start_time = time.time() #開始時間
coro = do_some_work(2)
loop = asyncio.get_event_loop() # 創建事件循環對象
loop.run_until_complete(coro)
print('運行