1. 区分并发与并行
并发与并行最主要的区别是:并发在同一时间只能执行一个任务,并行可以执行多个任务。
举例来说,优酷和爱奇艺都可以看电影,现在有电影 A 和电影 B:
并发:只用优酷或爱奇艺播放电影,不能同时播放电影 A 和电影 B;
并行:同时使用优酷和爱奇艺,可以同时播放电影 A 和电影 B。
常用的并发方式有协程和线程(协程也叫微线程,需要 python 并发时使用协程就好)
我们平时说 “多线程下载” 可以大大加快下载速度,主要是由于:
客户端(自己的电脑)发送下载请求后,服务器(网站)需要一段时间才能将请求的数据发送到客户端,中间有一段空白时间;
多线程下载时,将整个下载任务分为几个小任务,顺序发送各个小任务的数据请求,从而使等待服务器返回数据的时间有一定重合,从而减少整体等待时间,整体来看,就是加快了下载速度。
如下图:
注:数据量较大时(如 1GB 的视频),单线程下载时也并不是只发送一次请求,而是分成很多次,在客户端返回上一次请求的数据后再进行下一次请求。
注:多线程下载时,每个任务等待时间的开始时刻不一致,是因为在任务 1 发送请求处于等待时间内时,将任务 1 挂起,发送任务 2 的请求,以此类推……
2. 使用协程 asyncio 库
2.1. 简介
在使用协程之间,首先需可等待对象的概念。
可等待对象指可以在 await 语句中使用的对象;
执行多个协程任务时,如果遇见可等待对象,就可以将当前任务挂起,执行另一个任务。
如下图,图中 函数f_ 中并没有可等待对象,使用协程消耗的时间与不使用协程相同,大约为 4s。
从输出结果看,可知执行完毕 task_1 之后才开始执行 task_2。
如下图,图中 函数f_ 中含有可等待对象 asyncio.sleep(2),使用协程消耗的时间与不使用协程相同,大约为 2s。
从输出结果看,可知执行 task_1 遇到可等待对象则挂起 task_1,开始执行 task_2。
不含可等待对象的代码:
import asyncio
import time
async def f_(string: str):
print(f'START {string}')
time.sleep(2)
print(f'END {string}')
async def main():
start = time.time()
task_1 = f_('coroutine_1')
task_2 = f_('coroutine_2')
await asyncio.gather(task_1, task_2)
end = time.time()
print(f'time: {end-start}')
asyncio.run(main())
含可等待对象的代码:
import asyncio
import time
async def f_(string: str):
print(f'START {string}')
await asyncio.sleep(2)
print(f'END {string}')
async def main():
start = time.time()
task_1 = f_('coroutine_1')
task_2 = f_('coroutine_2')
await asyncio.gather(task_1, task_2)
end = time.time()
print(f'time: {end-start}')
asyncio.run(main())
2.2. 使用协程
协程函数的定义:使用 “async def function_name(args): ...” 的方式定义
协程执行的方式,根据是否在协程函数定义中,可分为有两类:
在普通函数定义中(即 `def function_name(args): ...`):
可使用 `asyncio.run(coroutine_name(args))` 执行,如上图中的 `asyncio.run(main())`
(asyncio.run 这个函数可以自动创建事件循环,并在协程执行完毕后关闭事件循环,不需要像以前的版本一样手动创建可关闭事件循环)
在协程函数定义中(即 `async def function_name(args): ...`):
可使用 await 关键字执行单个协程,
使用 `await asyncio.gather(task, task, ...)` 形式并发执行多个协程。
async def f():
await coroutine # 运行单个协程
await asyncio.gather(task, task, ...) # 并发执行多个任务
3. 使用异步请求库 aiohttp
requests 库为同步请求库,不能搭配 asyncio 并发发送请求;
aiohttp 库为异步请求库,非常适合与 asyncio 搭配,并发发送请求。
aiohttp 库用法与 requests 库相似度很高,这里仅介绍 get 请求函数:
async def declare_coroutine():
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(10)) as session:
async with session.get(url=str, params=dict) as resp:
resp.status # 响应状态码
resp.headers # 响应头
await resp.text() # 响应内容(文本格式)
await resp.read() # 响应内容(二进制格式)