最近正在學習Python中的異步編程,看了一些博客后做了一些小測驗:對比asyncio+aiohttp的爬蟲和asyncio+aiohttp+concurrent.futures(線程池/進程池)在效率中的差異,注釋:在爬蟲中我幾乎沒有使用任何計算性任務,為了探測異步的性能,全部都只是做了網絡IO請求,就是說aiohttp把網頁get完就程序就done了。
結果發現前者的效率比后者還要高。我詢問了另外一位博主,(提供代碼的博主沒回我信息),他說使用concurrent.futures的話因為我全部都是IO任務,如果把這些IO任務分散到線程池/進程池,反而多線程/多進程之間的切換開銷還會降低爬蟲的效率。我想了想的確如此。
那么我的問題是:僅僅在爬取網頁的過程中,就是request.get部分,多線程肯定是沒有存在的必要了,因為GIL這個大坑,進程池可能好點,但是性能還是不如異步爬蟲,而且更加浪費資源。既然這樣,是不是以后在爬蟲的爬取網頁階段我們完全都可以用興起的asyncio+aiohttp代替。(以及其他IO任務比如數據庫/文件讀寫)
當然在數據處理階段還是要采用多進程,但是我覺得多線程是徹底沒用了,原本它相比多進程的優勢在於IO型任務,現看來在它的優勢完全被異步取代了。(當然問題建立在不考慮兼容2.x)
注:還有一個額外的問題就是,看到一些博客說requests庫不支持異步編程是什么意思,為了充分發回異步的優勢應該使用aiohttp,我沒有看過requests的源代碼,但是一些結果顯示aiohttp的性能確實更好,各位網友能解釋一下嗎?
代碼
asyncio+aiohttp
import aiohttp
async def fetch_async(a):
async with aiohttp.request('GET', URL.format(a)) as r:
data = await r.json()
return data['args']['a']
start = time.time()
event_loop = asyncio.get_event_loop()
tasks = [fetch_async(num) for num in NUMBERS]
results = event_loop.run_until_complete(asyncio.gather(*tasks))
for num, result in zip(NUMBERS, results):
print('fetch({}) = {}'.format(num, result))
asyncio+aiohttp+線程池比上面要慢1秒
async def fetch_async(a):
async with aiohttp.request('GET', URL.format(a)) as r:
data = await r.json()
return a, data['args']['a']
def sub_loop(numbers):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = [fetch_async(num) for num in numbers]
results = loop.run_until_complete(asyncio.gather(*tasks))
for num, result in results:
print('fetch({}) = {}'.format(num, result))
async def run(executor, numbers):
await asyncio.get_event_loop().run_in_executor(executor, sub_loop, numbers)
def chunks(l, size):
n = math.ceil(len(l) / size)
for i in range(0, len(l), n):
yield l[i:i + n]
event_loop = asyncio.get_event_loop()
tasks = [run(executor, chunked) for chunked in chunks(NUMBERS, 3)]
results = event_loop.run_until_complete(asyncio.gather(*tasks))
print('Use asyncio+aiohttp+ThreadPoolExecutor cost: {}'.format(time.time() - start))
傳統的requests + ThreadPoolExecutor比上面慢了3倍
import time
import requests
from concurrent.futures import ThreadPoolExecutor
NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'
def fetch(a):
r = requests.get(URL.format(a))
return r.json()['args']['a']
start = time.time()
with ThreadPoolExecutor(max_workers=3) as executor:
for num, result in zip(NUMBERS, executor.map(fetch, NUMBERS)):
print('fetch({}) = {}'.format(num, result))
print('Use requests+ThreadPoolExecutor cost: {}'.format(time.time() - start))
補充
以上問題建立在CPython,至於我喜歡用多線程,不喜歡協程風格這類型的回答顯然不屬於本題討論范疇。我主要想請教的是:
如果Python拿不下GIL,我認為未來理想的模型應該是多進程 + 協程(asyncio+aiohttp)。uvloop和sanic以及500lines一個爬蟲項目已經開始這么干了。不討論兼容型問題,上面的看法是否正確,有一些什么場景協程無法取代多線程。
異步有很多方案,twisted, tornado等都有自己的解決方案,問題建立在asyncio+aiohttp的協程異步。
還有一個問題也想向各位網友請教一下
Python有了asyncio和aiohttp在爬蟲這類型IO任務中多線程/多進程還有存在的必要嗎? >> node.js
這個答案描述的挺清楚的:
http://www.goodpm.net/postreply/node.js/1010000007987098/Python有了asyncio和aiohttp在爬蟲這類型IO任務中多線程多進程還有存在的必要嗎.html