承接python從yield到asyncio<第四章>中提到的代碼問題。稍微修改一下代碼
# -*- coding: utf-8 -*-
# 讀取當當網的圖書
import requests
import aiohttp
import asyncio
from bs4 import BeautifulSoup
import time
import os
from concurrent.futures import ThreadPoolExecutor
# 子生成器
@asyncio.coroutine
def get_image(img_url):
yield from asyncio.sleep(1)
resp = yield from aiohttp.request('GET', img_url)
image = yield from resp.read()
return image
def save_image(img, img_url):
time.sleep(0.5)
with open(os.path.join('./img_file', img_url.split('/')[-1]), 'wb') as f:
f.write(img)
@asyncio.coroutine
def download_one(img_url):
image = yield from get_image(img_url)
save_image(image, img_url)
def thread_download_one(img_url):
time.sleep(1)
resp = requests.get(img_url)
image = resp.text
save_image(image, img_url)
if __name__ == '__main__':
images_list = [
'http://img3m0.ddimg.cn/67/4/24003310-1_b_5.jpg'
'http://img3m2.ddimg.cn/43/13/23958142-1_b_12.jpg',
'http://img3m0.ddimg.cn/60/17/24042210-1_b_5.jpg',
'http://img3m4.ddimg.cn/20/11/23473514-1_b_5.jpg',
'http://img3m4.ddimg.cn/40/14/22783504-1_b_1.jpg',
'http://img3m7.ddimg.cn/43/25/23254747-1_b_3.jpg',
'http://img3m9.ddimg.cn/30/36/23368089-1_b_2.jpg',
'http://img3m1.ddimg.cn/77/14/23259731-1_b_0.jpg',
'http://img3m2.ddimg.cn/33/18/23321562-1_b_21.jpg',
'http://img3m3.ddimg.cn/2/21/22628333-1_b_2.jpg',
'http://img3m8.ddimg.cn/85/30/23961748-1_b_10.jpg',
'http://img3m1.ddimg.cn/90/34/22880871-1_b_3.jpg',
'http://img3m2.ddimg.cn/62/27/23964002-1_b_6.jpg',
'http://img3m5.ddimg.cn/84/16/24188655-1_b_3.jpg',
'http://img3m6.ddimg.cn/46/1/24144166-1_b_23081.jpg',
'http://img3m9.ddimg.cn/79/8/8766529-1_b_0.jpg']
start = time.time()
loop = asyncio.get_event_loop()
to_do_tasks = [download_one(img) for img in images_list]
res, _ = loop.run_until_complete(asyncio.wait(to_do_tasks))
print(len(res))
print('asyncio cost:' + str(time.time() - start))
# ======================多線程版本===============================
start = time.time()
with ThreadPoolExecutor() as executor:
res = [executor.submit(thread_download_one, i) for i in images_list]
print(len(res))
print('Thread cost:' + time.time() - start)
代碼解讀
- 增加了多線程的下載函數thread_download_one, 和asyncio的方式一樣在http請求的時候阻塞1s
- 承接我們上一章的問題, 上一章的問題主要就是在save_image()函數, save_image操作硬盤保存文件, 控制權交還給主循環, 此刻有很多子生成器都返回了數據等待主線程的處理, 會導致主線程阻塞, 我們模擬耗時操作硬盤(休眠0.5s), 最終耗時8.63s, 而多線程耗時6.63s左右, asyncio比多線程效率更低了, 線程池多個線程并發的寫硬盤, 而此刻asyncio需要主線程處理完一個任務的寫硬盤操作之后才能處理下一個任務, 所以效率會很低。
知道了問題所在, 下一步要做的是改寫寫硬盤的操作, 這個操作不能阻塞主線程, asyncio也為我們提供了這樣的api, run_in_executor(), 該函數內部維護的是ThreadPoolExecutor線程池, 使用多線程的方式實現異步操作。
只需要改一下download_one函數
@asyncio.coroutine
def download_one(img_url):
image = yield from get_image(img_url)
loop = asyncio.get_event_loop()
loop.run_in_executor(None, save_image, image, img_url)
再次執行一下看一下運行時間。我執行1.3s, 相比于8.63s好了不少
補充:
- download_one函數中創建的loop循環對象和main函數中的loop對象是同一個, 可以看看源碼或者id()一下
- 主函數中不要loop.close(), run_in_executor函數每次都會調用self._check_closed()檢測循環是否關閉
3.書本中還介紹了yield from semaphore來限制并發請求數量, 由于asyncio不向多線程那樣阻塞, 加入循環事件任務被快速驅動, 并發訪問人家的網頁, 所以使用semaphore來及限制并發的數量, 讓你的程序溫柔對待他人的網站。這一塊可以結合書中的代碼學習, 這里不展開