1. 前置基础
1.1 什么是GIL?
2. 进程
3. 线程
4. 协程
4.1 非协程实例
首先来看非协程的代码实例
t1 = time.time()
def func1():
print("当前执行function 1")
time.sleep(1) # 当程序出现了同步操作的时候. 异步就中断了
print("当前执行function 1")
def func2():
print("当前执行function 2")
time.sleep(2)
print("当前执行function 2")
def func3():
print("当前执行function 3")
time.sleep(3)
print("当前执行function 3")
if __name__ == '__main__':
f1 = func1()
f2 = func2()
f3 = func3()
tasks = [
f1, f2, f3
]
# 一次性启动多个任务(协程)
# asyncio.run(asyncio.wait(tasks))
t2 = time.time()
print(t2 - t1)
结果
当前是function 1
当前是function 1
当前是function 2
当前是function 2
当前是function 3
当前是function 3
6.002589225769043
4.2 协程实例
async def func1():
print("当前执行function_1")
await asyncio.sleep(1)
print("当前执行function_1")
async def func2():
print("当前执行function_2")
await asyncio.sleep(2)
print("当前执行function_2")
async def func3():
print("当前执行function_3")
await asyncio.sleep(3)
print("当前执行function_3")
async def main():
# 第一种写法
# f1 = func1()
# await f1 # 一般await挂起操作放在协程对象前面
# 第二种写法(推荐)
tasks = [
asyncio.create_task(func1()), # py3.8以后加上asyncio.create_task()
asyncio.create_task(func2()),
asyncio.create_task(func3())
]
await asyncio.wait(tasks)
if __name__ == '__main__':
t1 = time.time()
# 一次性启动多个任务(协程)
asyncio.run(main())
t2 = time.time()
print(t2 - t1)
结果
当前执行function_1
当前执行function_2
当前执行function_3
当前执行function_1
当前执行function_2
当前执行function_3
3.0129427909851074
4.2.1 对比与发现
- 一共三个任务
- 非协程写法里
- 如果使用非协程写法,
time.sleep(1)
,将会导致IO阻塞。 因此程序会在设定的等待时间结束后, 才会往下执行。 - 耗时
6.002589225769043
- 打印的顺序自上而下
- 如果使用非协程写法,
- 协程写法里
- 耗时
3.0129427909851074
- 可以反推: 总耗时 = await 挂起时间最长的那个任务所花的时间(function 3) + 切换协程上下文的所需开销的时长(0.01)
- 耗时
- 异步协程的语法结构
4.3 异步协程最简实例
import asyncio
import aiohttp
import aiofiles
# 构造无数个urls
urls = [
"https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
"https://inews.gtimg.com/newsapp_bt/0/12171811596_909/0.png",
"http://www.soso.com/soso/images/logo_index_sosox2.png"
]
async def aiodownload(url):
# 异步下载功能的函数, 以下三个步骤都是IO操作
# 1.发送请求 aiohttp
# 2. 得到图片内容 async
# 3. 保存到文件 aiofiles
filename = url.rsplit("/", 1)[1]
async with aiohttp.ClientSession() as session: # 类似同步的 requests
async with session.get(url) as resp: # 类似同步的 resp = requests.get()
# 请求回来. 异步写入文件
async with aiofiles.open(filename, mode='wb') as f:
# await f.write(await resp.content.read()) # 读取内容是异步的. 需要await挂起, resp.text()
# Response对象的read()和text()方法会将响应一次性全部读入内存,这会导致内存爆满,导致卡顿,影响效率。 因此采取字节流的形式,每次读取4096个字节并写入文件。
while True:
pic_stream = await resp.content.read(4096)
if not pic_stream:
break
await f.write(pic_stream)
print(f'{filename} 已下载')
async def main():
# tasks = []
# for url in urls:
# # tasks.append(aiodownload(url))
# d = asyncio.create_task(aiodownload(url))
# tasks.append(d)
# 利用推导式的简写方式
tasks = [asyncio.create_task(aiodownload(url)) for url in urls]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
4.3.1 反思
在爬虫的三个基本操作都是涉及到IO
- 1.发送请求 aiohttp
- 2. 得到图片内容 async
- 3. 保存到文件 aiofiles
但2和1、3有显著区别。
- 磁盘io与网络io不同,磁盘顺序读写单个文件最快,并发读写会涉及到多个文件的切换问题,反而花了更多的时间,所以异步编程使用aiofiles要谨慎。
- 举一个有差异但基本原理类似的例子。在同一台电脑上, 将C盘里的文件复制到D盘去, 这里有个很明显的经验是如果同时只有一个这样的复制操作, 那么“较为高效”。如果“将C盘里的文件复制到D盘去”的同时, 开启多个文件转移复制粘贴进程, 速度极为缓慢甚至进程管理亲卡死。
- 解决的办法应该有很多种。
- 可以考虑把文件读写任务抽离出来,放到队列里面,然后用专门的线程或进程去按顺序去处理。
- 因此采取字节流的形式,每次读取4096个字节并写入文件
本站文章除单独注明外均为原创,本文链接https://bowmanjin.com/432,未经允许请勿转载。
请先
!