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 多線程效率不高嗎?

TAG:Python | 非同步 | asyncio |