承接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来及限制并发的数量, 让你的程序温柔对待他人的网站。这一块可以结合书中的代码学习, 这里不展开