Python中異步協程的使用方法介紹

1. 前言

在執行一些 IO 密集型任務的時候,程序經常會由於等待 IO 而阻塞。好比在網絡爬蟲中,若是咱們使用 requests 庫來進行請求的話,若是網站響應速度過慢,程序一直在等待網站響應,最後致使其爬取效率是很是很是低的。python

爲了解決這類問題,本文就來探討一下 Python 中異步協程來加速的方法,此種方法對於 IO 密集型任務很是有效。如將其應用到網絡爬蟲中,爬取效率甚至能夠成百倍地提高。express

注:本文協程使用 async/await 來實現,須要 Python 3.5 及以上版本。編程

2. 基本瞭解

在瞭解異步協程以前,咱們首先得了解一些基礎概念,如阻塞和非阻塞、同步和異步、多進程和協程。flask

2.1 阻塞

阻塞狀態指程序未獲得所需計算資源時被掛起的狀態。程序在等待某個操做完成期間,自身沒法繼續幹別的事情,則稱該程序在該操做上是阻塞的。服務器

常見的阻塞形式有:網絡 I/O 阻塞、磁盤 I/O 阻塞、用戶輸入阻塞等。阻塞是無處不在的,包括 CPU 切換上下文時,全部的進程都沒法真正幹事情,它們也會被阻塞。若是是多核 CPU 則正在執行上下文切換操做的核不可被利用。網絡

2.2 非阻塞

程序在等待某操做過程當中,自身不被阻塞,能夠繼續運行幹別的事情,則稱該程序在該操做上是非阻塞的。session

非阻塞並非在任何程序級別、任何狀況下均可以存在的。 僅當程序封裝的級別能夠囊括獨立的子程序單元時,它纔可能存在非阻塞狀態。多線程

非阻塞的存在是由於阻塞存在,正由於某個操做阻塞致使的耗時與效率低下,咱們纔要把它變成非阻塞的。app

2.3 同步

不一樣程序單元爲了完成某個任務,在執行過程當中需靠某種通訊方式以協調一致,稱這些程序單元是同步執行的。異步

例如購物系統中更新商品庫存,須要用「行鎖」做爲通訊信號,讓不一樣的更新請求強制排隊順序執行,那更新庫存的操做是同步的。

簡言之,同步意味着有序。

2.4 異步

爲完成某個任務,不一樣程序單元之間過程當中無需通訊協調,也能完成任務的方式,不相關的程序單元之間能夠是異步的。

例如,爬蟲下載網頁。調度程序調用下載程序後,便可調度其餘任務,而無需與該下載任務保持通訊以協調行爲。不一樣網頁的下載、保存等操做都是無關的,也無需相互通知協調。這些異步操做的完成時刻並不肯定。

簡言之,異步意味着無序。

2.5 多進程

多進程就是利用 CPU 的多核優點,在同一時間並行地執行多個任務,能夠大大提升執行效率。

2.6 協程

協程,英文叫作 Coroutine,又稱微線程,纖程,協程是一種用戶態的輕量級線程。

協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。所以協程能保留上一次調用時的狀態,即全部局部狀態的一個特定組合,每次過程重入時,就至關於進入上一次調用的狀態。

協程本質上是個單進程,協程相對於多進程來講,無需線程上下文切換的開銷,無需原子操做鎖定及同步的開銷,編程模型也很是簡單。

咱們可使用協程來實現異步操做,好比在網絡爬蟲場景下,咱們發出一個請求以後,須要等待必定的時間才能獲得響應,但其實在這個等待過程當中,程序能夠幹許多其餘的事情,等到響應獲得以後才切換回來繼續處理,這樣能夠充分利用 CPU 和其餘資源,這就是異步協程的優點。

3. 異步協程用法

接下來讓咱們來了解下協程的實現,從 Python 3.4 開始,Python 中加入了協程的概念,但這個版本的協程仍是以生成器對象爲基礎的,在 Python 3.5 則增長了 async/await,使得協程的實現更加方便。

Python 中使用協程最經常使用的庫莫過於 asyncio,因此本文會以 asyncio 爲基礎來介紹協程的使用。

首先咱們須要瞭解下面幾個概念:

  • [1] event_loop:事件循環,至關於一個無限循環,咱們能夠把一些函數註冊到這個事件循環上,當知足條件發生的時候,就會調用對應的處理方法。
  • [2] coroutine:中文翻譯叫協程,在 Python 中常指代爲協程對象類型,咱們能夠將協程對象註冊到時間循環中,它會被事件循環調用。咱們可使用 async 關鍵字來定義一個方法,這個方法在調用時不會當即被執行,而是返回一個協程對象。
  • [3] task:任務,它是對協程對象的進一步封裝,包含了任務的各個狀態。
  • [4] future:表明未來執行或沒有執行的任務的結果,實際上和 task 沒有本質區別。 另外咱們還須要瞭解 async/await 關鍵字,它是從 Python 3.5 纔出現的,專門用於定義協程。其中,async 定義一個協程,await 用來掛起阻塞方法的執行。

3.1 定義協程

首先咱們來定義一個協程,體驗一下它和普通進程在實現上的不一樣之處,代碼以下:

import asyncio
 
async def execute(x):
    print('Number:', x)
 
coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
 
loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

 

運行結果:

Coroutine: <coroutine object execute at 0x1034cf830>
After calling execute
Number: 1
After calling loop

 

首先咱們引入了 asyncio 這個包,這樣咱們纔可使用 async 和 await,而後咱們使用 async 定義了一個 execute() 方法,方法接收一個數字參數,方法執行以後會打印這個數字。

隨後咱們直接調用了這個方法,然而這個方法並無執行,而是返回了一個 coroutine 協程對象。隨後咱們使用 get_event_loop() 方法建立了一個事件循環 loop,並調用了 loop 對象的 run_until_complete() 方法將協程註冊到事件循環 loop 中,而後啓動。最後咱們纔看到了 execute() 方法打印了輸出結果。

可見,async 定義的方法就會變成一個沒法直接執行的 coroutine 對象,必須將其註冊到事件循環中才能夠執行。

上文咱們還提到了 task,它是對 coroutine 對象的進一步封裝,它裏面相比 coroutine 對象多了運行狀態,好比 running、finished 等,咱們能夠用這些狀態來獲取協程對象的執行狀況。

在上面的例子中,當咱們將 coroutine 對象傳遞給 run_until_complete() 方法的時候,實際上它進行了一個操做就是將 coroutine 封裝成了 task 對象,咱們也能夠顯式地進行聲明,以下所示:

import asyncio
 
async def execute(x):
    print('Number:', x)
    return x
 
coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
 
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

 

運行結果:

Coroutine: <coroutine object execute at 0x10e0f7830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

 

這裏咱們定義了 loop 對象以後,接着調用了它的 create_task() 方法將 coroutine 對象轉化爲了 task 對象,隨後咱們打印輸出一下,發現它是 pending 狀態。接着咱們將 task 對象添加到事件循環中獲得執行,隨後咱們再打印輸出一下 task 對象,發現它的狀態就變成了 finished,同時還能夠看到其 result 變成了 1,也就是咱們定義的 execute() 方法的返回結果。

另外定義 task 對象還有一種方式,就是直接經過 asyncio 的 ensure_future() 方法,返回結果也是 task 對象,這樣的話咱們就能夠不借助於 loop 來定義,即便咱們尚未聲明 loop 也能夠提早定義好 task 對象,寫法以下:

import asyncio
 
async def execute(x):
    print('Number:', x)
    return x
 
coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
 
task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

 

運行結果:

Coroutine: <coroutine object execute at 0x10aa33830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

 

發現其效果都是同樣的。

3.2 綁定回調

另外咱們也能夠爲某個 task 綁定一個回調方法,來看下面的例子:

import asyncio
import requests
 
async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status
 
def callback(task):
    print('Status:', task.result())
 
coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

 

在這裏咱們定義了一個 request() 方法,請求了百度,返回狀態碼,可是這個方法裏面咱們沒有任何 print() 語句。隨後咱們定義了一個 callback() 方法,這個方法接收一個參數,是 task 對象,而後調用 print() 方法打印了 task 對象的結果。這樣咱們就定義好了一個 coroutine 對象和一個回調方法,咱們如今但願的效果是,當 coroutine 對象執行完畢以後,就去執行聲明的 callback() 方法。

那麼它們兩者怎樣關聯起來呢?很簡單,只須要調用 add_done_callback() 方法便可,咱們將 callback() 方法傳遞給了封裝好的 task 對象,這樣當 task 執行完畢以後就能夠調用 callback() 方法了,同時 task 對象還會做爲參數傳遞給 callback() 方法,調用 task 對象的 result() 方法就能夠獲取返回結果了。

運行結果:

Task: <Task pending coro=<request() running at demo.py:5> cb=[callback() at demo.py:11]>
Status: <Response [200]>
Task: <Task finished coro=<request() done, defined at demo.py:5> result=<Response [200]>>

 

實際上不用回調方法,直接在 task 運行完畢以後也能夠直接調用 result() 方法獲取結果,以下所示:

import asyncio
import requests
 
async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status
 
coroutine = request()
task = asyncio.ensure_future(coroutine)
print('Task:', task)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('Task Result:', task.result())

 

運行結果是同樣的:

Task: <Task pending coro=<request() running at demo.py:4>>
Task: <Task finished coro=<request() done, defined at demo.py:4> result=<Response [200]>>
Task Result: <Response [200]>

 

3.3 多任務協程

上面的例子咱們只執行了一次請求,若是咱們想執行屢次請求應該怎麼辦呢?咱們能夠定義一個 task 列表,而後使用 asyncio 的 wait() 方法便可執行,看下面的例子:

import asyncio
import requests
 
async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)
 
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
for task in tasks:
    print('Task Result:', task.result())

 

這裏咱們使用一個 for 循環建立了五個 task,組成了一個列表,而後把這個列表首先傳遞給了 asyncio 的 wait() 方法,而後再將其註冊到時間循環中,就能夠發起五個任務了。最後咱們再將任務的運行結果輸出出來,運行結果以下:

Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

 

能夠看到五個任務被順次執行了,並獲得了運行結果。

3.4 協程實現

前面說了這麼一通,又是 async,又是 coroutine,又是 task,又是 callback,但彷佛並無看出協程的優點啊?反而寫法上更加奇怪和麻煩了,別急,上面的案例只是爲後面的使用做鋪墊,接下來咱們正式來看下協程在解決 IO 密集型任務上有怎樣的優點吧!

上面的代碼中,咱們用一個網絡請求做爲示例,這就是一個耗時等待的操做,由於咱們請求網頁以後須要等待頁面響應並返回結果。耗時等待的操做通常都是 IO 操做,好比文件讀取、網絡請求等等。協程對於處理這種操做是有很大優點的,當遇到須要等待的狀況的時候,程序能夠暫時掛起,轉而去執行其餘的操做,從而避免一直等待一個程序而耗費過多的時間,充分利用資源。

爲了表現出協程的優點,咱們須要先建立一個合適的實驗環境,最好的方法就是模擬一個須要等待必定時間才能夠獲取返回結果的網頁,上面的代碼中使用了百度,但百度的響應太快了,並且響應速度也會受本機網速影響,因此最好的方式是本身在本地模擬一個慢速服務器,這裏咱們選用 Flask。

若是沒有安裝 Flask 的話能夠執行以下命令安裝:

pip3 install flask

 

而後編寫服務器代碼以下:

from flask import Flask
import time
 
app = Flask(__name__)
 
@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'
 
if __name__ == '__main__':
    app.run(threaded=True)

 

這裏咱們定義了一個 Flask 服務,主入口是 index() 方法,方法裏面先調用了 sleep() 方法休眠 3 秒,而後接着再返回結果,也就是說,每次請求這個接口至少要耗時 3 秒,這樣咱們就模擬了一個慢速的服務接口。

注意這裏服務啓動的時候,run() 方法加了一個參數 threaded,這代表 Flask 啓動了多線程模式,否則默認是隻有一個線程的。若是不開啓多線程模式,同一時刻遇到多個請求的時候,只能順次處理,這樣即便咱們使用協程異步請求了這個服務,也只能一個一個排隊等待,瓶頸就會出如今服務端。因此,多線程模式是有必要打開的。

啓動以後,Flask 應該默認會在 127.0.0.1:5000 上運行,運行以後控制檯輸出結果以下:

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 

接下來咱們再從新使用上面的方法請求一遍:

import asyncio
import requests
import time
 
start = time.time()
 
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
end = time.time()
print('Cost time:', end - start)

 

在這裏咱們仍是建立了五個 task,而後將 task 列表傳給 wait() 方法並註冊到時間循環中執行。

運行結果以下:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.049368143081665

 

能夠發現和正常的請求並無什麼兩樣,依然仍是順次執行的,耗時 15 秒,平均一個請求耗時 3 秒,說好的異步處理呢?

其實,要實現異步處理,咱們得先要有掛起的操做,當一個任務須要等待 IO 結果的時候,能夠掛起當前任務,轉而去執行其餘任務,這樣咱們才能充分利用好資源,上面方法都是一本正經的串行走下來,連個掛起都沒有,怎麼可能實現異步?想太多了。

要實現異步,接下來咱們再瞭解一下 await 的用法,使用 await 能夠將耗時等待的操做掛起,讓出控制權。當協程執行的時候遇到 await,時間循環就會將本協程掛起,轉而去執行別的協程,直到其餘的協程掛起或執行完畢。

因此,咱們可能會將代碼中的 request() 方法改爲以下的樣子:

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await requests.get(url)
    print('Get response from', url, 'Result:', response.text)

 

僅僅是在 requests 前面加了一個 await,然而執行如下代碼,會獲得以下報錯:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Cost time: 15.048935890197754
Task exception was never retrieved
future: <Task finished coro=<request() done, defined at demo.py:7> exception=TypeError("object Response can't be used in 'await' expression",)>
Traceback (most recent call last):
  File "demo.py", line 10, in request
    status = await requests.get(url)
TypeError: object Response can't be used in 'await' expression

 

此次它遇到 await 方法確實掛起了,也等待了,可是最後卻報了這麼個錯,這個錯誤的意思是 requests 返回的 Response 對象不能和 await 一塊兒使用,爲何呢?由於根據官方文檔說明,await 後面的對象必須是以下格式之一:

  • [1] A native coroutine object returned from a native coroutine function,一個原生 coroutine 對象。
  • [2] A generator-based coroutine object returned from a function decorated with types.coroutine(),一個由 types.coroutine() 修飾的生成器,這個生成器能夠返回 coroutine 對象。
  • [3] An object with an await__ method returning an iterator,一個包含 __await 方法的對象返回的一個迭代器。 能夠參見:https://www.python.org/dev/peps/pep-0492/#await-expression。 reqeusts 返回的 Response 不符合上面任一條件,所以就會報上面的錯誤了。

那麼有的小夥伴就發現了,既然 await 後面能夠跟一個 coroutine 對象,那麼我用 async 把請求的方法改爲 coroutine 對象不就能夠了嗎?因此就改寫成以下的樣子:

import asyncio
import requests
import time
 
start = time.time()
 
async def get(url):
    return requests.get(url)
 
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response.text)
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
end = time.time()
print('Cost time:', end - start)

 

這裏咱們將請求頁面的方法獨立出來,並用 async 修飾,這樣就獲得了一個 coroutine 對象,咱們運行一下看看:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.134317874908447

 

仍是不行,它還不是異步執行,也就是說咱們僅僅將涉及 IO 操做的代碼封裝到 async 修飾的方法裏面是不可行的!咱們必需要使用支持異步操做的請求方式才能夠實現真正的異步,因此這裏就須要 aiohttp 派上用場了。

3.5 使用 aiohttp

aiohttp 是一個支持異步請求的庫,利用它和 asyncio 配合咱們能夠很是方便地實現異步請求操做。

安裝方式以下:

pip3 install aiohttp

 

官方文檔連接爲:https://aiohttp.readthedocs.io/,它分爲兩部分,一部分是 Client,一部分是 Server,詳細的內容能夠參考官方文檔。

下面咱們將 aiohttp 用上來,將代碼改爲以下樣子:

import asyncio
import aiohttp
import time
 
start = time.time()
 
async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result
 
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)
 
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
 
end = time.time()
print('Cost time:', end - start)

 

在這裏咱們將請求庫由 requests 改爲了 aiohttp,經過 aiohttp 的 ClientSession 類的 get() 方法進行請求,結果以下:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 3.0199508666992188

 

成功了!咱們發現此次請求的耗時由 15 秒變成了 3 秒,耗時直接變成了原來的 1/5。

代碼裏面咱們使用了 await,後面跟了 get() 方法,在執行這五個協程的時候,若是遇到了 await,那麼就會將當前協程掛起,轉而去執行其餘的協程,直到其餘的協程也掛起或執行完畢,再進行下一個協程的執行。

開始運行時,時間循環會運行第一個 task,針對第一個 task 來講,當執行到第一個 await 跟着的 get() 方法時,它被掛起,但這個 get() 方法第一步的執行是非阻塞的,掛起以後立馬被喚醒,因此當即又進入執行,建立了 ClientSession 對象,接着遇到了第二個 await,調用了 session.get() 請求方法,而後就被掛起了,因爲請求須要耗時好久,因此一直沒有被喚醒,好第一個 task 被掛起了,那接下來該怎麼辦呢?事件循環會尋找當前未被掛起的協程繼續執行,因而就轉而執行第二個 task 了,也是同樣的流程操做,直到執行了第五個 task 的 session.get() 方法以後,所有的 task 都被掛起了。全部 task 都已經處於掛起狀態,那咋辦?只好等待了。3 秒以後,幾個請求幾乎同時都有了響應,而後幾個 task 也被喚醒接着執行,輸出請求結果,最後耗時,3 秒!

怎麼樣?這就是異步操做的便捷之處,當遇到阻塞式操做時,任務被掛起,程序接着去執行其餘的任務,而不是傻傻地等着,這樣能夠充分利用 CPU 時間,而沒必要把時間浪費在等待 IO 上。

有人就會說了,既然這樣的話,在上面的例子中,在發出網絡請求後,既然接下來的 3 秒都是在等待的,在 3 秒以內,CPU 能夠處理的 task 數量遠不止這些,那麼豈不是咱們放 10 個、20 個、50 個、100 個、1000 個 task 一塊兒執行,最後獲得全部結果的耗時不都是 3 秒左右嗎?由於這幾個任務被掛起後都是一塊兒等待的。

理論來講確實是這樣的,不過有個前提,那就是服務器在同一時刻接受無限次請求都能保證正常返回結果,也就是服務器無限抗壓,另外還要忽略 IO 傳輸時延,確實能夠作到無限 task 一塊兒執行且在預想時間內獲得結果。

咱們這裏將 task 數量設置成 100,再試一下:

tasks = [asyncio.ensure_future(request()) for _ in range(100)]

 

耗時結果以下:

Cost time: 3.106252670288086

 

最後運行時間也是在 3 秒左右,固然多出來的時間就是 IO 時延了。

可見,使用了異步協程以後,咱們幾乎能夠在相同的時間內實現成百上千倍次的網絡請求,把這個運用在爬蟲中,速度提高可謂是很是可觀了。

3.6 與單進程、多進程對比

可能有的小夥伴很是想知道上面的例子中,若是 100 次請求,不是用異步協程的話,使用單進程和多進程會耗費多少時間,咱們來測試一下:

首先來測試一下單進程的時間:

import requests
import time
 
start = time.time()
 
def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)
 
for _ in range(100):
    request()
 
end = time.time()
print('Cost time:', end - start)

 

最後耗時:

Cost time: 305.16639709472656

 

接下來咱們使用多進程來測試下,使用 multiprocessing 庫:

import requests
import time
import multiprocessing
 
start = time.time()
 
def request(_):
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)
 
cpu_count = multiprocessing.cpu_count()
print('Cpu count:', cpu_count)
pool = multiprocessing.Pool(cpu_count)
pool.map(request, range(100))
 
end = time.time()
print('Cost time:', end - start)

 

這裏我使用了multiprocessing 裏面的 Pool 類,即進程池。個人電腦的 CPU 個數是 8 個,這裏的進程池的大小就是 8。

運行時間:

Cost time: 48.17306900024414 

可見 multiprocessing 相比單線程來講,仍是能夠大大提升效率的。

3.7 與多進程的結合

既然異步協程和多進程對網絡請求都有提高,那麼爲何不把兩者結合起來呢?在最新的 PyCon 2018 上,來自 Facebook 的 John Reese 介紹了 asyncio 和 multiprocessing 各自的特色,並開發了一個新的庫,叫作 aiomultiprocess,感興趣的能夠了解下:https://www.youtube.com/watch?v=0kXaLh8Fz3k。

這個庫的安裝方式是:

pip3 install aiomultiprocess

 

須要 Python 3.6 及更高版本纔可以使用。

使用這個庫,咱們能夠將上面的例子改寫以下:

import asyncio
import aiohttp
import time
from aiomultiprocess import Pool
 
start = time.time()
 
async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result
 
async def request():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result
 
coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
 
end = time.time()
print('Cost time:', end - start)

 

這樣就會同時使用多進程和異步協程進行請求,固然最後的結果其實和異步是差很少的:

Cost time: 3.1156570434570312

 

由於個人測試接口的緣由,最快的響應也是 3 秒,因此這部分多餘的時間基本都是 IO 傳輸時延。但在真實狀況下,咱們在作爬取的時候遇到的狀況變幻無窮,一方面咱們使用異步協程來防止阻塞,另外一方面咱們使用 multiprocessing 來利用多核成倍加速,節省時間其實仍是很是可觀的。

轉載自:靜覓 » Python中異步協程的使用方法介紹

相關文章
相關標籤/搜索