python異步asyncio模塊的使用

本文首發於知乎
異步是繼多線程、多進程以後第三種實現併發的方式,主要用於IO密集型任務的運行效率提高。python中的異步基於yield生成器,在講解這部分原理以前,咱們先學會異步庫asyncio的使用。html

本文主要講解asyncio模塊的通用性問題,對一些函數細節的使用就簡單略過。python

本文分爲以下部分編程

  • 最簡單的使用
  • 另外一種常見的使用方式
  • 一個問題
  • 通常函數下的異步
  • 理解異步、協程
  • 單個線程的的異步爬蟲

最簡單的使用

import asyncio
async 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秒。網絡

上面代碼一些約定俗成的用法記住就好,如session

  • 要想異步運行函數,須要在定義函數時前面加async
  • 後三行都是記住就行,到時候把函數傳入

另外一種常見的使用方式

上面是第一種常見的用法,下面是另一種多線程

import asyncio
async 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,當前當作徹底等價便可,因此平時使用用上面哪一種均可以。併發

上面是最常看到的兩種使用方式,這裏列出來保證讀者在看其餘文章時不會發蒙。app

另外,兩者實際上是有細微差異的異步

  • gather更擅長於將函數聚合在一塊兒
  • wait更擅長篩選運行情況

細節能夠參考這篇回答async

一個問題

與以前學過的多線程、多進程相比,asyncio模塊有一個很是大的不一樣:傳入的函數不是爲所欲爲

  • 好比咱們把上面myfun函數中的sleep換成time.sleep(1),運行時則不是異步的,而是同步,共等待了10秒
  • 若是我換一個myfun,好比換成下面這個使用request抓取網頁的函數
import asyncio
import requests
from bs4 import BeautifulSoup
async 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 asyncio
import time
def 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):
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
複製代碼

上面run_in_executor其實開啓了新的線程,再協調各個線程。調用過程比較複雜,只要當模板同樣套用便可。

上面10次循環仍然不是一次性打印出來的,而是像分批次同樣打印出來的。這是由於開啓的線程不夠多,若是想要實現一次打印,能夠開啓10個線程,代碼以下

import concurrent.futures as cf # 多加一個模塊
import asyncio
import time
def 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):
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
複製代碼

用這種方法實現requests異步爬蟲代碼以下

import concurrent.futures as cf
import asyncio
import requests
from bs4 import BeautifulSoup
def 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):
pass
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
複製代碼

這部分參考這篇文章還有這個回答

這種開啓多個線程的方式也算異步的一種,下面一節詳細解釋。

理解異步、協程

如今咱們講了一些異步的使用,是時候解釋一些概念了

  • 首先,咱們要理清楚同步、異步、阻塞、非阻塞四個詞語之間的聯繫
  • 首先要明確,前二者後後二者並非一一對應的,它們不是在說同一件事情,可是很是相似,容易搞混
  • 通常咱們說異步程序是非阻塞的,而同步既有阻塞也有非阻塞的
  • 非阻塞是指一個任務沒作完,沒有必要停在那裏等它結束就能夠開始下一個任務,保證一直在幹活沒有等待;阻塞就相反是一件事徹底結束纔開始另外一件事
  • 在非阻塞的狀況下,同步與異步都有可能,它們均可以在一個任務沒結束就開啓下一個任務。而兩者的區別在於:(且稱正在進行的程序爲主程序)當第一個程序作完的時候(好比網絡請求終於相應了),會自動通知主程序回來繼續操做第一個任務的結果,這種是異步;而同步則是須要主程序不斷去問第一個程序是否已經完成。
  • 四個詞的區別參考知乎回答
  • 協程與多線程的區別)在非阻塞的狀況下,多線程是同步的表明,協程是異步的表明。兩者都開啓了多個線程
  • 多線程中,多個線程會競爭誰先運行,一個等待結束也不會去通知主程序,這樣沒有章法的隨機運行會形成一些資源浪費
  • 而協程中,多個線程(稱爲微線程)的調用和等待都是經過明確代碼組織的。協程就像目標明確地執行一個又一個任務,而多線程則有一些彷徨迷茫的時間
  • 兩種異步
  • 前面幾節涉及到兩種異步,一種是await只使用一個線程就能夠實現任務切換,另外一種是開啓了多個線程,經過線程調度實現異步
  • 通常只用一個線程將任務在多個函數之間來回切換,是使用yield生成器實現的,例子能夠看這篇文章最後生產消費者例子
  • 多進程、多線程、異步擅長方向
  • 異步和多線程都是在IO密集型任務上優點明顯,由於它們的本質都是在儘可能避免IO等待時間形成的資源浪費。而多進程能夠利用多核優點,適合CPU密集型任務
  • 相比於多線程,異步更適合每次等待時間較長、須要等待的任務較多的程序。由於多線程畢竟要建立新的線程,線程過多使線程競爭現象更加明顯,資源浪費也就更多。若是每一個任務等待時間過長,等待時間內勢必開啓了很是多任務,很是多線程,這時使用多線程就不是一個明智的決定。而異步則能夠只開啓一個線程在各個任務之間有條不紊進行,即能充分利用CPU資源,又不會影響程序運行效率

單個線程的的異步爬蟲

上面咱們是經過開啓多個線程來實現requests的異步,若是咱們想只用一個線程(用await),就要換一個網頁請求函數。

事實上要想用await,必須是一個awaitable對象,這是不能使用requests的緣由。而轉化成awaitable對象這樣的事固然也不用咱們本身實現,如今有一個aiohttp模塊能夠將網頁請求和asyncio模塊完美對接。使用這個模塊改寫代碼以下

import asyncio
import aiohttp
from bs4 import BeautifulSoup
async 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編程

專欄目錄:目錄

版本說明:軟件及包版本說明

相關文章
相關標籤/搜索