python非同步asyncio模塊的使用
非同步是繼多線程、多進程之後第三種實現並發的方式,主要用於IO密集型任務的運行效率提升。python中的非同步基於yield
生成器,在講解這部分原理之前,我們先學會非同步庫asyncio的使用。
本文主要講解asyncio
模塊的通用性問題,對一些函數細節的使用就簡單略過。
本文分為如下部分
- 最簡單的使用
- 另一種常見的使用方式
- 一個問題
- 一般函數下的非同步
- 理解非同步、協程
- 單個線程的的非同步爬蟲
最簡單的使用
import asyncioasync def myfun(i): print(start {}th.format(i)) await asyncio.sleep(1) print(finish {}th.format(i))loop = asyncio.get_event_loop()myfun_list = (myfun(i) for i in range(10))loop.run_until_complete(asyncio.gather(*myfun_list))
這樣運行,10次等待總共只等待了1秒。
上面代碼一些約定俗成的用法記住就好,如
- 要想非同步運行函數,需要在定義函數時前面加
async
- 後三行都是記住就行,到時候把函數傳入
另一種常見的使用方式
上面是第一種常見的用法,下面是另外一種
import asyncioasync def myfun(i): print(start {}th.format(i)) await asyncio.sleep(1) print(finish {}th.format(i))loop = asyncio.get_event_loop()myfun_list = [asyncio.ensure_future(myfun(i)) for i in range(10)]loop.run_until_complete(asyncio.wait(myfun_list))
這種用法和上面一種的不同在於後面調用的是asyncio.gather
還是asyncio.wait
,當前看成完全等價即可,所以平時使用用上面哪種都可以。
上面是最常看到的兩種使用方式,這裡列出來保證讀者在看其他文章時不會發矇。
另外,二者其實是有細微差別的
gather
更擅長於將函數聚合在一起wait
更擅長篩選運行狀況
細節可以參考這篇回答
一個問題
與之前學過的多線程、多進程相比,asyncio
模塊有一個非常大的不同:傳入的函數不是隨心所欲
- 比如我們把上面
myfun
函數中的sleep
換成time.sleep(1)
,運行時則不是非同步的,而是同步,共等待了10秒 - 如果我換一個
myfun
,比如換成下面這個使用request
抓取網頁的函數
import asyncioimport requestsfrom bs4 import BeautifulSoupasync def get_title(a): url = https://movie.douban.com/top250?start={}&filter=.format(a*25) r = requests.get(url) soup = BeautifulSoup(r.content, html.parser) lis = soup.find(ol, class_=grid_view).find_all(li) for li in lis: title = li.find(span, class_="title").text print(title)loop = asyncio.get_event_loop()fun_list = (get_title(i) for i in range(10))loop.run_until_complete(asyncio.gather(*fun_list))
依然不會非同步執行。
到這裡我們就會想,是不是非同步只對它自己定義的sleep
(await asyncio.sleep(1)
)才能觸發非同步?
一般函數下的非同步
對於上述函數,asyncio
庫只能通過添加線程的方式實現非同步,下面我們實現time.sleep
時的非同步
import asyncioimport timedef myfun(i): print(start {}th.format(i)) time.sleep(1) print(finish {}th.format(i))async def main(): loop = asyncio.get_event_loop() futures = ( loop.run_in_executor( None, myfun, i) for i in range(10) ) for result in await asyncio.gather(*futures): passloop = asyncio.get_event_loop()loop.run_until_complete(main())
上面run_in_executor
其實開啟了新的線程,再協調各個線程。調用過程比較複雜,只要當模板一樣套用即可。
上面10次循環仍然不是一次性列印出來的,而是像分批次一樣列印出來的。這是因為開啟的線程不夠多,如果想要實現一次列印,可以開啟10個線程,代碼如下
import concurrent.futures as cf # 多加一個模塊import asyncioimport timedef myfun(i): print(start {}th.format(i)) time.sleep(1) print(finish {}th.format(i))async def main(): with cf.ThreadPoolExecutor(max_workers = 10) as executor: # 設置10個線程 loop = asyncio.get_event_loop() futures = ( loop.run_in_executor( executor, # 按照10個線程來執行 myfun, i) for i in range(10) ) for result in await asyncio.gather(*futures): passloop = asyncio.get_event_loop()loop.run_until_complete(main())
用這種方法實現requests
非同步爬蟲代碼如下
import concurrent.futures as cfimport asyncioimport requestsfrom bs4 import BeautifulSoupdef get_title(i): url = https://movie.douban.com/top250?start={}&filter=.format(i*25) r = requests.get(url) soup = BeautifulSoup(r.content, html.parser) lis = soup.find(ol, class_=grid_view).find_all(li) for li in lis: title = li.find(span, class_="title").text print(title)async def main(): with cf.ThreadPoolExecutor(max_workers = 10) as executor: loop = asyncio.get_event_loop() futures = ( loop.run_in_executor( executor, get_title, i) for i in range(10) ) for result in await asyncio.gather(*futures): passloop = asyncio.get_event_loop()loop.run_until_complete(main())
這部分參考這篇文章還有這個回答
這種開啟多個線程的方式也算非同步的一種,下面一節詳細解釋。
理解非同步、協程
現在我們講了一些非同步的使用,是時候解釋一些概念了
同步、非同步、阻塞、非阻塞四個詞語之間的聯繫
- 首先要明確,前兩者後後兩者並不是一一對應的,它們不是在說同一件事情,但是非常類似,容易搞混
- 一般我們說非同步程序是非阻塞的,而同步既有阻塞也有非阻塞的
- 非阻塞是指一個任務沒做完,沒有必要停在那裡等它結束就可以開始下一個任務,保證一直在幹活沒有等待;阻塞就相反是一件事完全結束才開始另一件事
- 在非阻塞的情況下,同步與非同步都有可能,它們都可以在一個任務沒結束就開啟下一個任務。而二者的區別在於:(且稱正在進行的程序為主程序)當第一個程序做完的時候(比如網路請求終於相應了),會自動通知主程序回來繼續操作第一個任務的結果,這種是非同步;而同步則是需要主程序不斷去問第一個程序是否已經完成。
- 四個詞的區別參考知乎回答
協程與多線程的區別
- 在非阻塞的情況下,多線程是同步的代表,協程是非同步的代表。二者都開啟了多個線程
- 多線程中,多個線程會競爭誰先運行,一個等待結束也不會去通知主程序,這樣沒有章法的隨機運行會造成一些資源浪費
- 而協程中,多個線程(稱為微線程)的調用和等待都是通過明確代碼組織的。協程就像目標明確地執行一個又一個任務,而多線程則有一些彷徨迷茫的時間
兩種非同步
- 前面幾節涉及到兩種非同步,一種是
await
只使用一個線程就可以實現任務切換,另一種是開啟了多個線程,通過線程調度實現非同步 - 一般只用一個線程將任務在多個函數之間來回切換,是使用yield生成器實現的,例子可以看這篇文章最後生產消費者例子
多進程、多線程、非同步擅長方向
- 非同步和多線程都是在IO密集型任務上優勢明顯,因為它們的本質都是在盡量避免IO等待時間造成的資源浪費。而多進程可以利用多核優勢,適合CPU密集型任務
- 相比於多線程,非同步更適合每次等待時間較長、需要等待的任務較多的程序。因為多線程畢竟要創建新的線程,線程過多使線程競爭現象更加明顯,資源浪費也就更多。如果每個任務等待時間過長,等待時間內勢必開啟了非常多任務,非常多線程,這時使用多線程就不是一個明智的決定。而非同步則可以只開啟一個線程在各個任務之間有條不紊進行,即能充分利用CPU資源,又不會影響程序運行效率
單個線程的的非同步爬蟲
上面我們是通過開啟多個線程來實現requests
的非同步,如果我們想只用一個線程(用await
),就要換一個網頁請求函數。
事實上要想用await
,必須是一個awaitable對象,這是不能使用requests
的原因。而轉化成awaitable對象這樣的事當然也不用我們自己實現,現在有一個aiohttp
模塊可以將網頁請求和asyncio
模塊完美對接。使用這個模塊改寫代碼如下
import asyncioimport aiohttpfrom bs4 import BeautifulSoupasync def get_title(i): url = https://movie.douban.com/top250?start={}&filter=.format(i*25) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: print(resp.status) text = await resp.text() print(start, i) soup = BeautifulSoup(text, html.parser) lis = soup.find(ol, class_=grid_view).find_all(li) for li in lis: title = li.find(span, class_="title").text print(title)loop = asyncio.get_event_loop()fun_list = (get_title(i) for i in range(10))loop.run_until_complete(asyncio.gather(*fun_list))
專欄信息
專欄主頁:python編程
專欄目錄:目錄
版本說明:軟體及包版本說明
推薦閱讀:
※做網路工程師還是做大數據,請求大佬指路?
※Python 中 a+=b 和 a=a+b 的區別有哪些?
※學完 VB 後學什麼編程語言更好?
※【Python3網路爬蟲開發實戰】3.1.1-發送請求
※Python 多線程效率不高嗎?