Python 爬蟲提速:【多進程、多線程、協程+異步】對比測試

概念介紹

首先簡單介紹幾個概念:

  • 進程和線程
    • 進程就是一個程序在一個數據集上的一次動態執行過程(數據集是程序在執行過程中所需要使用的資源)。
    • 線程也叫輕量級進程,它是一個基本的 CPU 執行單元,是比進程更小的能獨立運行的基本單位。
    • 進程和線程的關係
      • 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。
      • 資源分配給進程,同一進程的所有線程共享該進程的所有資源。
      • CPU 分給線程,即真正在 CPU 上運行的是線程。
  • 並行和併發
    • 並行處理是計算機系統中能同時執行兩個或更多個處理的一種計算方法。並行處理可同時工作於同一程序的不同方面,其主要目的是節省大型和複雜問題的解決時間。
    • 併發處理指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個 CPU 上運行,但任一個時刻點上只有一個程序在 CPU 上運行。
    • 併發的關鍵是你有處理多個任務的能力,不一定要同時。並行的關鍵是你有同時處理多個任務的能力。所以說,並行是併發的子集多進程是並行的,多線程是併發的
  • 同步和異步
    • 同步就是指一個進程在執行某個請求的時候,若該請求需要一段時間才能返回信息,那麼這個進程將會一直等待下去,直到收到返回信息才繼續執行下去。
    • 異步是指進程不需要一直等下去,而是繼續執行下面的操作,不管其他進程的狀態。當有消息返回時系統會通知進程進行處理,這樣可以提高執行的效率。
    • 舉個例子,打電話時就是同步通信,發短息時就是異步通信。

測試環境

進行對比測試之前,我們先來創建一個合適的實驗環境:
       模擬一個需要等待一定時間纔可以獲取返回結果的網頁。
如果直接用百度、CSDN 等站點的話,一方面響應太快、難以看出各種方法的差距,另一方面響應速度會受網速影響、每次發送請求獲取響應所需的時間不完全一致導致重複實驗結果差距較大,所以在此用 Flask 模擬一個本地慢速服務器。
flask_server.py 代碼如下:

from flask import Flask     # pip install flask
import time

app = Flask(__name__)

@app.route('/')
def index():
    time.sleep(3)    		# 休眠 3 秒再返回結果
    return 'Hello!'

if __name__ == '__main__':
    app.run(threaded=True)  # 以多線程模式啓動服務

啓動之後,Flask 服務默認在 127.0.0.1:5000 上運行,控制檯輸出結果如下:

* Serving Flask app "flask_server" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

在瀏覽器中訪問 http://127.0.0.1:5000/ 等待 3 秒即會出現 Hello! 頁面,表明簡單的慢速服務器搭建完成!
Hello!

開始測試

首先導入需要的模塊,以請求 10 次爲例準備 urls,再定義一個 get_html_text() 函數:

import requests
import time
# 用於多進程
from multiprocessing import Process
# 用於多線程
from threading import Thread
# 用於協程+異步 
import asyncio
import aiohttp      # pip install aiohttp

urls = ['http://127.0.0.1:5000' for _ in range(10)]

def get_html_text(url):
    response = requests.get(url)
    return response.text

測試【單進程單線程】

start = time.time()
for url in urls:
    result = get_html_text(url)
    print(result)
end = time.time()
print('【單進程單線程】耗時:%s 秒' %(end - start))

結果如下:

Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
【單進程單線程】耗時:30.15605854988098 秒

測試【多進程 並行】

start = time.time()
processes = []
for url in urls:
    p = Process(target=get_html_text, args=(url,))
    processes.append(p)
    p.start()
for p in processes:
    p.join()
    print('Hello!')
end = time.time()
print('【多進程 並行】耗時:%s 秒' %(end - start))

結果如下(測試電腦爲 4 核 CPU,核心數越大加速越明顯):

Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
【多進程  並行】耗時:5.511234283447266 秒

測試【多線程 併發】

start = time.time()
threads = []
for url in urls:
    t = Thread(target=get_html_text, args=(url,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
    print('Hello!')
end = time.time()
print('【多線程 併發】耗時:%s 秒' %(end - start))

結果如下:

Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
【多線程  併發】耗時:3.030653953552246 秒

測試【協程 + 異步】

# 因爲 requests 模塊不支持異步操作,需要藉助 aiohttp 模塊
async def get_html_text_async(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            text = await response.text()
            return text

start = time.time()
tasks = [asyncio.ensure_future(get_html_text_async(url)) for url in urls]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
for task in tasks:
    print(task.result())
end = time.time()
print('【協程 ++ 異步】耗時:%s 秒' %(end - start))

結果如下:

Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
Hello!
【協程 ++ 異步】耗時:3.046288251876831 秒

結果對比

len(urls)==1:
n=1
len(urls)==4:
n=4
len(urls)==10:
n=10
len(urls)==100:
m=100

  • 單進程單線程是將 n 次請求順次執行,每次要等待 3 秒才能返回結果,故耗時 3n+ 秒;
  • 多進程-並行處理則利用 CPU 的多核優勢,在同一時間並行地執行多個任務,可以大大提高執行效率,但系統實現多進程前需要一些準備工作、將耗費大量時間。
  • 多線程-併發處理協程+異步的耗時由單進程單線程的 3n+ 秒變成了 3+ 秒!
    • 前者是 n 個請求幾乎同時進行、幾乎同時得到響應返回結果。
    • 後者是每當請求任務遇到阻塞(time.sleep(3))時被掛起,n 個任務都處於掛起狀態後等待 3 秒,n 個請求幾乎同時都有了響應,然後掛起的任務被喚醒接着執行,輸出請求結果,最後耗時:3 秒!(多出來的時間是 IO 時延)

注意:

  • 搭建的實驗環境是慢速服務器,若不進行 time.sleep(3) 休眠 3 秒再返回 而是立即響應的話,單進程單線程的實際耗時則會大大縮短,請求次數少的話甚至會超過多進程。
  • 而且筆者在 Windows 4 核 CPU 環境下測試,最多開啓 4 個進程,未能發揮多進程的真實實力。
  • 另外,多進程、多線程還可以通過進程池、線程池來實現,與文中方法耗時基本一致,故未做展示;多進程或多線程與協程異步IO結合的效率尚待測試。

繪圖展示

子圖
總圖