前言
前面的文章提到過,python使用多線程,會因為GIL的原因導致多線程的使用效率低下,甚至比單個線程的處理速度還慢。然而在python編程中, 為了解決多線程之間上下文切換的開銷,以及增加線程控制的靈活性,python引入了協程 。本文我們就來說一說python協程的特點和使用方法。
?
一、協程定義
定義:協程(Coroutine),又稱微線程。協程的作用,是在執行函數A時,可以隨時中斷,去執行函數B,然后中斷繼續執行函數A(可以自由切換)。但這一過程并不是函數調用(沒有調用語句),這一整個過程看似像多線程,然而協程只有一個線程執行。
無論多線程和多進程,IO的調度更多取決于系統,而 協程的方式,調度來自用戶 ,用戶可以在函數中yield一個狀態。使用協程可以實現高效的并發任務。
?
二、協程的實現
Python的在3.4中引入了協程的概念,可是這個還是以生成器對象為基礎,3.5則確定了協程的語法。
實現協程通常可以引入一些python庫來實現,其中最常用的就是asyncio,當然也有一些其他的如tornado和gevent都實現了類似的功能。這里我們就來了解一下asyncio
協程通過 async/await 語法進行聲明 ,是編寫異步應用的推薦方式。我們先來了解asyncio的幾個概念名詞:
event_loop 事件循環:程序開啟一個無限的循環,程序員會把一些函數注冊到事件循環上。當滿足事件發生的時候,調用相應的協程函數。
coroutine 協程:協程對象,指一個使用async關鍵字定義的函數,它的調用不會立即執行函數,而是會返回一個協程對象。協程對象需要注冊到事件循環,由事件循環調用。
task 任務:一個協程對象就是一個原生可以掛起的函數,任務則是對協程進一步封裝,其中包含任務的各種狀態。
future: 代表將來執行或沒有執行的任務的結果。它和task上沒有本質的區別
async/await 關鍵字:python3.5 用于定義協程的關鍵字,async定義一個協程,await用于掛起阻塞的異步調用接口。
為了解釋上面的名詞,我們一步步來實現簡單的異步編程:
1、定義一個協程
import time
import asyncio
# 定義一個協程并創建一個事件循環
loop = asyncio.get_event_loop()
# 協程對象coroutine需要在事件循環里面才能執行
loop.run_until_complete(coroutine)
2、協程對象
async def sync_func(x):
print('Waiting: ', x)
# 實例化一個協程對象
coroutine = sync_func(2)
由上面可以得出, 用async修飾的方法是一個協程對象,協程對象需要在協程的事件循環里面才能運行 。
3、創建一個task
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)
事件循環除了可以運行協程對象,也可以運行任務。
在這里 task只是對協程對象coroutine進行了封裝,task還可以綁定協程對象執行后的回調,甚至還可以封裝成list數組等 ,逐個進行事件循環調用。
4、task綁定回調函數
def callback(x):
print(x)
task = asyncio.ensure_future(coroutine)
task.add_done_callback(functools.partial(callback, 2))
loop.run_until_complete(task)
其中callback是回調函數,執行完協程之后會執行回調函數。綁定回調也是用task封裝協程對象的一個好處
5、協程的阻塞和await
async def sync_func(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return 'Done after {}s'.format(x)
coroutine = sync_func(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)
如上所述,如果協程對象函數里面帶有 await ,是可以實現協程的運行阻塞的。上面例子執行結果是print之后sleep了2秒,這 期間可以讓出控制器,loop可以去調用別的協程 ,然后再回來執行return,return的結果可以用task.result()來獲取。
6、多個協程并發執行
async def sync_func(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return 'Done after {}s'.format(x)
start = now()
coroutine1 = sync_func(1)
coroutine2 = sync_func(2)
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2)
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
上述例子執行結果為:協程1print之后阻塞,再協程2print后也阻塞,最后協程1return,協程2再return。
7、協程停止:啟動事件循環之后,馬上ctrl+c,會觸發run_until_complete的執行異常 KeyBorardInterrupt。然后通過循環asyncio.Task取消future。
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
loop.stop()
loop.run_forever()
finally:
loop.close()
?
三、協程的使用場景
1、當一個程序變得很大而且復雜時,將其劃分為子程序,每一部分實現特定的任務是個不錯的方案。 子程序不能單獨執行,只能在主程序的請求下執行,主程序負責協調使用各個子程序 。協程就是子程序的泛化。和子程序一樣的事,協程只負責計算任務的一步;和子程序不一樣的是,協程沒有主程序來進行調度。
2、異步爬蟲
很多關心協程的朋友,大部分是用其寫爬蟲,這是因為協程能很好的解決IO阻塞問題。那么對于異步爬蟲的需求,使用協程的方法大致如下:
(1)grequests;
(2)爬蟲模塊+gevent;
(3)aiohttp;
(4)scrapy框架+asyncio模塊
3、協程池
類似于gevent,我們可以先創建好協程,放入一個協程池中,每次有任務請求的時候都由協程去執行,主線程進行統一管理和調度,這在一下I/O密集型的數據清洗等工作中可以提高很多效率。
?
四、協程的優缺點
1、執行效率高,因為子程序切換(函數)不是線程切換,由程序自身控制,沒有切換線程的開銷。所以與多線程相比,線程的數量越多,協程性能的優勢越明顯;
2、不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在控制共享資源時也不需要加鎖,因此執行效率高很多。
3、缺點: 無法利用多核資源 ,協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上。
4、 進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程序
參考鏈接:
https://thief.one/2017/02/20/Python%E5%8D%8F%E7%A8%8B/
https://www.jianshu.com/p/7690edfe9ba5
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
