协程
文章目录
- 协程
- 协程的优劣势
- 什么是IO密集型任务
- 特点
- 示例
- 与 CPU 密集型任务的对比
- 处理 I/O 密集型任务的方式
- 总结
- 创建并使用协程
- asyncio模块
- 创建协程函数
- 运行协程函数asyncio.run(main())
- aiohttp模块
- 调用aiohttp模块步骤
- aiofiles————协程异步函数
- 遇到的问题
- 一 await asyncio.wait(task)的作用
- 1. **理解 `await` 的作用**
- 2. **`asyncio.wait(task)` 的作用**
- 3. **启动多个任务**
- 多协程案例——爬取图片
- 完整代码
- 设计思路
- 按顺序解读代码
- 主函数
🌟 亲爱的读者们, 🌟
感谢你们访问我的博客!如果你希望更深入、透彻地了解文章中的内容,欢迎随时私信我。我可以为你录制视频讲解,让知识变得更加生动有趣。
当然,若你们对这个想法感兴趣的朋友们越多,我会更有动力去制作这些视频哦!让我们一起探索更多的知识,共同成长!
期待你的私信!📩
协程(coroutine):也叫作微线程,纤程,协程是单线程下的并发。
协程的作用:在执行函数A时,随时中断去执行函数B,然后再中断函数B.返回来执行函数A,该操作类似多线程,但协程中只有一个线程在执行。
微观上:一个一个任务的切换,切换条件一般是IO操作
宏观上:是多个任务在同时执行——多任务异步操作
协程的优劣势
-
协程的优势:
非常适用于I/O密集型任务;
执行效率高(切换函数,而不切换线程,没有多余开销)
不需要锁机制。
-
协程的劣势:
无法利用多核资源
进行阻塞操作会阻塞掉整个程序。
什么是IO密集型任务
I/O 密集型任务(I/O-bound task)是指那些在执行过程中主要受限于输入/输出(I/O)操作性能的任务。这类任务的执行效率往往取决于数据的读写速度,而不是 CPU 处理能力。
特点
- 依赖于外部设备:I/O 密集型任务常常涉及与外部设备的交互,如硬盘、网络、数据库等。
- 等待时间长:由于 I/O 操作(如文件读取、网络请求)通常比 CPU 操作慢,I/O 密集型任务在执行时可能会有较长的等待时间。
- CPU 空闲:在等待 I/O 操作完成时,CPU 可能处于空闲状态,未能充分利用计算资源。
示例
- 文件读取/写入:长时间读取或写入大文件。
- 网络请求:等待远程服务器的响应,如 HTTP 请求。
- 数据库操作:查询或写入数据库的过程,这些操作通常涉及网络通信和磁盘访问。
与 CPU 密集型任务的对比
- I/O 密集型任务:主要依赖于 I/O 操作,CPU 通常在执行期间处于等待状态。
- CPU 密集型任务:主要依赖于计算和处理,通常会占用大量的 CPU 资源。
处理 I/O 密集型任务的方式
- 采用 异步编程:如使用回调或 Promises 来处理 I/O 操作,而不是阻塞主线程。
- 利用 多线程或多进程:同时处理多个 I/O 操作,以减少整体等待时间。
总结
I/O 密集型任务是在执行中高度依赖于数据交换和处理速度的任务。理解 I/O 密集型任务的概念有助于在设计和优化系统时选择合适的编程模型和架构。
因为爬虫主要就是和网络进行交互,不太消耗我们自己电脑的CPU,负责接收网路返回的数据,是一种写入操作,就是适合爬虫
创建并使用协程
asyncio模块
python的标准库
- 创建协程函数
async def function()
await IO操作 - 调用协程函数
async def main()
await function() - 并发执行多个协程:
asyncio.create task() - 运行协程:
asyncio.run(main())
import asyncio # 导入模块
import time
# 创建协程函数
async def singing():
print('start singing')
time.sleep(2)
print('end singing')
async def dancing():
print('start dance')
time.sleep(3)
print('end dance')
async def main():
# 调用协程函数
await singing()
await dancing()
if __name__ == '__main__':
start_time = time.time()
# 运行协程函数
asyncio.run(main())
end_time = time.time()
print(f'time:{end_time-start_time}')
start singing
end singing
start dance
end dance
time:5.002954006195068
发现是5秒,就是串行
上面的这个程序是协程程序,但是是单协程,也就是串行,一个一个来
创建协程函数
asyncio.create_task(singing())
await asyncio.wait([task1,task2])
import asyncio # 导入模块
import time
# 创建协程函数
async def singing():
print('start singing')
await asyncio.sleep(2)
print('end singing')
async def dancing():
print('start dance')
await asyncio.sleep(3)
print('end dance')
async def main():
# 使用协程里面的任务函数,来创建并发多协程
task1 = asyncio.create_task(singing())
task2 = asyncio.create_task(dancing())
await asyncio.wait([task1,task2])
if __name__ == '__main__':
start_time = time.time()
# 运行协程函数
asyncio.run(main())
end_time = time.time()
print(f'time:{end_time-start_time}')
start singing
start dance
end singing
end dance
time:3.0030245780944824
和多线程一样,都是有一个模块自己的函数,来启动线程/协程
time是一个同步函数,不能让CPU异步执行,即便是使用了并发执行多任务函数,CPU也不能同时运行
使用await调用协程函数自带的sleep函数
CPU遇到await语句,并且发生了堵塞,比如网络请求还没到,或者这个sleep等待函数未执行完,CPU都会自己切换出去
运行协程函数asyncio.run(main())
import asyncio # 导入模块
import time
# 创建协程函数
async def singing():
print('start singing')
await asyncio.sleep(2)
print('end singing')
async def dancing():
print('start dance')
await asyncio.sleep(3)
print('end dance')
async def main():
# 和多线程一样,可以把要启动的多协程任务放到一个列表里面,最后在启动列表
task = []
for i in range(3):
task.append(asyncio.create_task(singing()))
task.append(asyncio.create_task(dancing()))
await asyncio.wait(task) # 启动协程
if __name__ == '__main__':
start_time = time.time()
# 运行协程函数
asyncio.run(main())
end_time = time.time()
print(f'time:{end_time-start_time}')
start singing
start dance
start singing
start dance
start singing
start dance
end singing
end singing
end singing
end dance
end dance
end dance
time:3.003164768218994
和多线程一样,先是用asyncio模块下面的create task也就是字面意思,创建一个多协程任务,把协程函数写在里面
外面使用for循环,控制循环次数,也就是协程的数量,每循环一次,就会在这个create task里面多两个任务
所以循环了三次,就有6个任务被创建
因为这是协程任务,里面的sleep就是阻塞,所以程序一直在切换协程
aiohttp模块
相当于异步requests模块,适合在协程中使用使用方法类似 requests模块
官网:https://docs.aiohttp.org/en/stable/client_quickstart.html
requests是同步模块在协程中不能及时的释放CPU,所以需要使用协程专用的aiohttp模块
调用aiohttp模块步骤
- async with 是关键字
- aiohttp.ClientSession() 这是一个方法,然后起名字
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as r: 前面两个是关键字,后面的是请求方式,和requests.get()一样
- 返回方式有三种:text,content,json
- 这里打印返回值的时候还要在后面加一个括号,r.text(),r.centent.read(),调用centent的时候还要加一个read()后缀,r.json()
import asyncio
import aiohttp
async def downloads(url):
# print(url)
name = url.split('/')[-1]
# print(name)
async with aiohttp.ClientSession() as session:
async with session.get(url) as r: # 这里是把得到的网络请求内容赋值给r
with open(fr'E:\python笔记\{name}','wb') as f:
f.write(await r.content.read()) # 这里需要使用等待关键字,因为是网络请求,网络响应的速度比不上CPU运算的速度
print(f'已完成……{name}')
async def main():
urls = ('https://i1.huishahe.com/uploads/allimg/202302/9999/bcc80e5d24.jpg',
'https://i1.huishahe.com/uploads/allimg/202302/9999/8ccff5510c.jpg',
'https://i1.huishahe.com/uploads/allimg/202302/9999/186094180f.jpg')
task = []
for url in urls:
task.append(asyncio.create_task(downloads(url))) # 把协程任务存进列表
await asyncio.wait(task) # 启动列表内的任务,并告诉协程这是一个等待语句
if __name__ == '__main__':
asyncio.run(main())
设计思路:
- 设计协程函数入口
- 把协程函数写入协程任务
- 使用await 运行协程任务
- 设计协程函数
- async with aiohttp.ClientSession() as session这个是aiohttp的方法,必须要写
- async with session.get(url) as r 这个就是异步网络请求了,返回内容在r里面
aiofiles————协程异步函数
也是协程里面的函数,也是异步函数,可以主动等待网络的返回值
import asyncio
from os import write
import aiofiles
import aiohttp
async def downloads(url):
# print(url)
name = url.split('/')[-1]
# print(name)
async with aiohttp.ClientSession() as session:
async with session.get(url) as r: # 这里是把得到的网络请求内容赋值给r
# with open(fr'E:\python笔记\{name}','wb') as f:
# f.write(await r.content.read()) # 这里需要使用等待关键字,因为是网络请求,网络响应的速度比不上CPU运算的速度
# print(f'已完成……{name}')
async with aiofiles.open(fr'E:\python笔记\{name}','wb') as f:
await f.write(await r.content.read())
print(f'已完成……{name}')
和原来的文件保存方式相似,就是换了个关键字
- 因为要使用协程函数,所以要使用async关键字
- 因为网络请求需要等待,可能阻塞,还要写await让程序挂起
遇到的问题
一 await asyncio.wait(task)的作用
在使用 Python 的 asyncio
库时,协程的调度和执行是通过事件循环来处理的。await asyncio.wait(task)
是用来等待一组任务完成的关键工具。下面是它的重要性和工作原理的详细解释:
1. 理解 await
的作用
await
用于暂停协程的执行,直到被await
的任务完成。它允许事件循环在等待的同时执行其他任务,从而提高效率。- 在
async
函数中, 使用await
的时候,协程会被挂起,控制权返回到事件循环,从而允许其他协程运行。
2. asyncio.wait(task)
的作用
asyncio.wait
是一个并发等待工具,它接受一个任务或任务列表,直到这些任务中的一个或全部完成。它返回一个集合,包含已完成和未完成的任务,方便后续处理。- 通过等待任务的完成,我们可以获得每个任务的结果或获取异常信息。
3. 启动多个任务
- 使用
asyncio.wait
启动和管理多个协程任务。例如,当有多个协程需要并发执行时,可以将它们封装在一个任务列表中,然后调用asyncio.wait
来调度。 - 这样可以让我们更好地控制任务的执行,以及处理结果和异常。
多协程案例——爬取图片
效果图
完整代码
import pprint
import aiofiles
import aiohttp
import asyncio
# 使用BS4解析网页
from bs4 import BeautifulSoup
from requests.packages import target
# 获取翻页地址
async def get_all_url(url):
# print(url)
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
# print(await r.text()) # 可等待对象-----网页源代码
soup = BeautifulSoup(await r.text(),'html.parser')
links = soup.find('div',class_='slist').find_all('a',target="_blank") # 从目录页得到的总链接
return_links = []
for link in links:
return_links.append(link['href'])
return return_links
# 获取目录页图片地址
async def get_url(url):
# print(url)
async with aiohttp.ClientSession() as session:
async with session.get(url) as r:
soup = BeautifulSoup(await r.text(), 'html.parser')
pic_address = soup.find('div',class_="photo-pic").find('img')['src']
title = soup.find('div',class_="photo-pic").find('img')['title']
# print(pic_address,title)
return pic_address,title
# 下载图片
async def download_pic(jpg_address,title):
# print(jpg_address,title)
async with aiohttp.ClientSession() as session:
async with session.get(jpg_address) as r:
# print(r) # ok
async with aiofiles.open(fr'E:\python笔记\{title}.jpg','wb') as f:
await f.write(await r.content.read())
print(f'已完成……{title}')
async def main():
task1 = []
for i in range(1,2):
if i == 1:
url = 'https://www.moyublog.com/hdwallpapers/meinv/index.html'
else:
url = f'https://www.moyublog.com/hdwallpapers/meinv/index_{i}.html'
task1.append(asyncio.create_task(get_all_url(url)))
dones,pending = await asyncio.wait(task1) # await 关键字会返回两个内容,第一个是在函数里面return的
# for link in dones: # 这里的dones返回的是一个列表,在改变目录页数的时候,列表的数量也会改变
# print(link.result()) # 当你发起异步操作时,该操作可能尚未完成。任务对象在执行完成前只是一个占位符。只有在它们完成后,调用 result() 才能获取实际的返回值。
# 不调用 result() 直接打印任务对象,会返回任务的信息(如状态),而不是实际的返回值。
task2 = []
for link in dones:
pages = link.result() # 把一个列表储存在pages里面
for rt in pages: # 这里是在使用rt读取一个pages列表,最后打印出来
# print(rt) # 发现得到网页正常
task2.append(asyncio.create_task(get_url(rt)))
dones,pending = await asyncio.wait(task2)
task3 = []
for t in dones:
# print(t.result()) # 发现返回的内容都在元组里面
jpg_address,title = t.result() # 可以直接自定义两个参数接收元组里面的内容
# print(jpg_address,title) # ok
task3.append(asyncio.create_task(download_pic(jpg_address,title)))
await asyncio.wait(task3)
if __name__ == '__main__':
asyncio.run(main())
设计思路
- 找个比较合适的网站
- 找个你感兴趣的分类
- 获取目录页网址,自己找规律组合其他的目录页
- 获取目录页里面的每一种图片网址
- 从图片的网址找到图片的链接
- 下载并保存图片
按顺序解读代码
先要清楚,这里是多协程代码,会有网络请求等语句,也就是程序堵塞的地方,都要使用aio的关键字,比如异步网络请求aiohttp,异步文件储存aiofiles,异步函数执行async,异步函数的运行asyncio.wait,还有异步任务的创建asyncio.creata_task()
- 找目录页里面的图片链接的时,首先按照aiohttp.ClientSession()的方法,创建session,在使用sesssion.get的方式向网络发送请求,这里的session.get的用法和requests.get一样
- 在上一步已经把返回的内容储存在对象r里面了
- 这里使用了BS4的语法进行文件解析,也算是练习一下BS4的语法
- 在这里获取到了图片页的链接
我们获得了图片页的链接和名称,这个协程的任务就结束了,现在我们把这两个内容返回出去
这是一个协程函数,它的返回内容有两个dones和pending,前者是已完成的内容,后者是未完成的内容
想要正常获取协程函数的返回值,我们需要使用这两个参数结束返回值,后面的参数不用管
我们对前面的参数使用result()方法解析,就可以正常获取内容了
这里我们得到了很多列表,也就是一个目录页里面的所有链接在一个列表里面
把这个列表传递给下一个函数get_url这个函数的工作是专门从图片页的链接得到图片的jpg文件和名称
同样也是协程函数,和上面的处理方式一样
再把图片的jpg链接和名称给下载及保存图片的函数,这里就使用了异步文件保存的方式,因为网络请求有一定的延迟,还要使用await关键字
主函数
我们使用for循环构造目录页的链接,并存入协程任务
然后是把链接给处理目录页的函数
再把返回内容给图片页函数
再把返回内容给下载图片的函数
这些都是异步协程操作,也就是I/O密集型任务
所以可以在极短的时间保存大量的图片,比之前的多线程快了很多
await
在 asyncio.wait
中并不会直接返回 dones
和 pendings
两个参数。相反,await asyncio.wait(...)
返回的是一个包含两个集合的元组:
done
: 包含所有已完成的 Task 对象。pending
: 包含所有未完成的 Task 对象。
研究的好几天才能较为熟练的使用协程函数,就是比较麻烦,但是因为是异步加载的请求,所以可以不用在等待网络返回内容时一直占用CPU了,CPU会去执行其他的协程