跟着武沛齐老师教程做的学习笔记
主要参考:https://pythonav.com/wiki/detail/6/91/
课程链接(《asyncio异步编程》,讲师:武沛齐老师)
1 协程
1.1 简介
协程,又称微线程,英文名Coroutine。
协程是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块相互切换执行(协程可以利用IO等待的时间去执行一些其他的代码,从而提升代码执行效率。)。
协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
引自:https://www.jianshu.com/p/6dde7f92951e
协程的执行最终靠的还是线程,协程切换非常快,成本低,一般占用栈大小远小于线程(协程 KB 级别,线程 MB 级别),所以可以开更多的协程。
因此简言之, 进程可以包含多个线程,多个线程共享进程的资源,因此线程比进程更轻量;而协程的本质是一个函数,一个线程可以包含多个协程,协程比线程更轻量。
例如
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()
一般的执行顺序是先执行 fun1 ,完了之后在执行 fun2 ,先后会输出:1、2、3、4
例如这种串行的方式
来源:网络
如果是协程就会在代码之间进行切换,比如由协程执行时,在执行 fun1 的过程中,可以随时中断,去执行 fun2 ,fun2 也可能在执行过程中中断再去执行 fun2,来回切换执行 ,最终可能输入:1、3、2、4
这就是这句话的意思
协程是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行。
上述例子看起来 fun1 、fun2 的执行有点像多线程,但协程的特点在于是一个线程执行,即协程是一个线程执行,因此协程具有极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
1.2 在Python中实现协程方式
-
greenlet,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现)
-
yield,生成器,借助生成器的特点也可以实现协程代码。
-
asyncio,在Python3.4中引入的模块用于编写协程代码。
-
async & awiat,在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码【官方推荐】。
1.2.1 greenlet
安装
pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com greenlet
下面由一个线程在执行代码的时候,在多个函数之间来回切换执行
from greenlet import greenlet
def func1():
print(1) # 第1步:输出 1
gr2.switch() # 第3步:切换到 func2 函数
print(2) # 第6步:输出 2
gr2.switch() # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行
def func2():
print(3) # 第4步:输出 3
gr1.switch() # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
print(4) # 第8步:输出 4
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第1步:去执行 func1 函数
1.2.2 yield
生成器,借助生成器的特点也可以实现协程代码。
def func1():
yield 1
yield from func2()
yield 2
def func2():
yield 3
yield 4
f1 = func1()
# 每循环一次相当于执行func1中一次yield
for item in f1:
print(item)
1.2.3 asyncio
asyncio模块是专门用于实现协程的
加了 @asyncio.coroutine 装饰器,由普通函数变为协程函数,执行时必须通过
loop = asyncio.get_event_loop()
loop.run_until_complete( fun1)
才能正常的执行
import asyncio
# 装饰器 coroutine 协程函数
@asyncio.coroutine
def func1():
print(1)
yield from asyncio.sleep(1) # 遇到IO耗时操作,在等待的过程中,自动化切换到tasks中的其他任务
print(2)
@asyncio.coroutine
def func2():
print(3)
yield from asyncio.sleep(2) # 遇到IO耗时操作,在等待的过程中,自动化切换到tasks中的其他任务
print(4)
# task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态
# 创建一个task
# 协程对象不能直接运行,在注册事件循环的时候,
# 其实是run_until_complete方法将协程包装成为了一个任务(task)对象.
# task对象是Future类的子类,保存了协程运行后的状态,用于未来获取协程的结果
tasks = [
asyncio.ensure_future( func1() ),
asyncio.ensure_future( func2() )
]
# event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
# asyncio.get_event_loop:创建一个事件循环,
# 然后使用run_until_complete将协程注册到事件循环,并启动事件循环
#
# 创建一个事件loop
loop = asyncio.get_event_loop()
# 把fun1 和fun2 打包,会同时执行这两个函数
# 将协程加入到事件循环loop
# 创建task后,在task加入事件循环之前为pending状态,当完成后,状态为finished
loop.run_until_complete(asyncio.wait(tasks))
asyncio 遇到IO耗时操作(有网络就有IO,比如通过爬虫访问网址下载图片),在等待的过程中,自动化切换到tasks中的其他任务
大大提高了执行效率
1.2.4 async & awit
本质和上面差不多,只是为了让代码更加简洁,进入async & awit这两个关键字
@asyncio.coroutine 装饰器就会被移除,推荐使用async & awit 关键字实现协程代码。
@asyncio.coroutine ==》 async
yield from ==》 await
import asyncio
async def func1():
print(1)
await asyncio.sleep(2)
print(2)
async def func2():
print(3)
await asyncio.sleep(2)
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
因此,根据以上所有,可以把协程理解为在一个线程中如果遇到IO等待时间,不会等,会利用空闲的时间干点其他事。
压榨剩余价值,不让它闲着
1.2.5 普通爬虫与协程爬虫
插曲:因为async & awit是Python3.8之后的,我使用的python版本是3.7,又不想升级,所以单独又装了一个3.9,在IDLE上运行这部分代码
1.2.5.1 普通爬虫(同步:按照顺序逐一排队执行)
import requests
import time
def download_image(url):
print("开始下载:",url)
# 发送网络请求,下载图片
response = requests.get(url)
print("下载完成")
# 图片保存到本地文件
file_name = url.rsplit('_')[-1]
with open(file_name, mode='wb') as file_object:
file_object.write(response.content)
if __name__ == '__main__':
url_list = [
'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
]
start_time=time.time()
for item in url_list:
download_image(item)
end_time=time.time()
print('耗时:',end_time-start_time)
1.2.5.2 基于协程的异步编程实现(异步:遇到IO请求自动切换去发送其他任务请求)
使用 aiohttp 获取
import aiohttp
import asyncio
async def fetch(session, url):
print("发送请求:", url)
# 发送请求,IO有等待的时间,不会等待,继续发送第二个请求
# 获取响应 response
async with session.get(url, verify_ssl=False) as response:
content = await response.content.read()
# 图片保存到本地文件
file_name = url.rsplit('_')[-1]
with open(file_name, mode='wb') as file_object:
file_object.write(content)
print('下载完成',url)
async def main():
async with aiohttp.ClientSession() as session:
url_list = [
'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
]
tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
先发送一个请求,不等返回结果,又发送下一个请求…
执行效率显著提高
2 异步编程
2.1 事件循环
理解成一个死循环,去检测并执行任务列表中的任务。
当我们定义了一个任务列表时,任务列表中的每个任务都会有一个状态,比如说可执行,等待执行(即IO操作,程序会忽略掉此任务,等它的状态变为可执行时才执行此任务),执行完成等。事件循环会先去任务列表中检查任务的状态,并将状态为可执行和已完成的任务返回,然后再去循环返回的可执行任务列表执行任务,循环已完成任务列表将已完成任务从任务列表中移除,然后进行下一次的检测,直至任务列表为空是终止循环。
(引自:https://blog.csdn.net/weixin_46297209/article/details/111464854)
# 伪代码
任务列表 = [ 任务1, 任务2, 任务3,... ]
while True:
可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回
for 就绪任务 in 已准备就绪的任务列表:
执行已就绪的任务
for 已完成的任务 in 已完成的任务列表:
在任务列表中移除 已完成的任务
如果 任务列表 中的任务都已完成,则终止循环
在异步编程中,事件循环的获取与创建
import asyncio
# 生成 or 获取 一个事件循环
loop = asynico.get_event_loop()
# 将任务加入到任务列表,即事件循环中,去检测任务的状态,是否是可运行,是否是IO
loop.run_until_complete(tasks)
2.2 async关键字(普通函数==>协程函数)
协程函数: async def
async def func1():
print(1)
await asyncio.sleep(2)
print(2)
协程对象:执行 协程函数() 得到的协程对象
re = func1()
# re为协程对象
注意:与普通函数不同,执行协程函数创建协程对象时,协程函数内部代码不会执行,只是获取一个协程对象,必须与事件循环配合使用,即将协程对象交给事件循环处理。
即
如果想要运行协程函数内部代码,必须将它放入事件循环中
例
import asyncio
async def fun1():
print('哈哈哈哈哈')
# 生成 or 获取 一个事件循环
loop = asyncio.get_event_loop()
# 将任务加入到任务列表,即事件循环中,去检测任务的状态,是否是可运行,是否是IO
loop.run_until_complete(fun1())
上述写法还是太麻烦
python3.7 之后,有了更简便的写法
import asyncio
async def fun1():
print('哈哈哈哈哈')
# 调用协程函数
asyncio.run(fun1())
2.3 await关键字(等)
await + 可等待对象(协程对象,Future,Task对象),即等待IO对象
遇到 await关键字 即进入 IO等待(比如说向服务器发起请求等待响应返回内容),如果有其他的任务,此时不会等待而是会切换到其他任务,如果这个等待完成,则会继续执行下面的操作
注意:await 后面只能跟特定的对象
import asyncio
async def myfun1():
print("你猜~")
await asyncio.sleep(2)
print("这都猜不到,我是你的小可爱啊!")
return "生气了"
async def myfun2():
print("你是谁?")
# 将await等待的对象赋值给response
# 遇到 IO等待时,会切换到其他协程对象
# await 等 == myfun1()有返回值了 才执行下面的代码
response = await myfun1()
print("你怎么了?\n", response)
asyncio.run(myfun2())
await 就是等待对象的值得到结果之后再继续向下走。
import asyncio
async def myfun1():
print("你猜~")
await asyncio.sleep(2)
print("这都猜不到,我是你的小可爱啊!")
return "生气了"
async def myfun2():
print("你是谁?")
response1 = await myfun1()
print("你怎么了?\n", response1)
print('-'*10)
response2 = await myfun1()
print("你怎么了?\n", response2)
asyncio.run(myfun2())
注意:这里任务列表中只有一个任务,在等待 response1 = await myfun1()
时,不会执行其他的,这里只有一个任务
2.4 Tasks对象
在事件循环中添加多个任务,即向任务列表中添加任务,用于并发调度协程。
通过 asyncio.create_task(协程对象)
的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。
import asyncio
async def func():
print("start")
await asyncio.sleep(1)
print("end")
return "返回值"
async def main():
print("main开始")
# 创建Task对象,将当前执行func函数任务添加到事件循环
# 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
task1 = asyncio.create_task(func())
# 创建Task对象,将当前执行func函数任务添加到事件循环
# 此时任务列表中有三个任务,分别为main,func,func
task2 = asyncio.create_task(func())
print("main结束")
# main任务遇到了await task1,意味着mian任务要等待task1执行完并且有了返回值之后才能再往下执行
# 忽略等待状态的任务,切换到task1,即执行func函数,
# 输出start后进入等待状态,进入task2任务,即再次执行func函数
# 输出start后进入等待状态,进入task1任务,输出end,并返回函数值
# task1任务结束,从任务列表中进行删除,任务列表中只剩下main任务以及执行了一半的task2任务
# response1接受到返回值,main任务开始往下执行
response1 = await task1
# main任务遇到await task2,意味着main任务要等待task2执行完毕并且有了返回值之后再往下执行
# 切换到task2任务,从上次等待的地方往下执行,输出end,并返回函数值
# task2任务结束,response2接受返回值,main任务继续往下执行
response2 = await task2
# 打印task1和task2的返回值response1和response2
print(response1, response2)
if __name__ == '__main__':
# 将main函数放入到任务列表中
asyncio.run(main())
代码:从概念到案例全面剖析协程
该博主分析的很好!
更常见的写法是把 task1 与 task2 …,这些任务放到一个列表中,
task1 = asyncio.create_task(func())
task2 = asyncio.create_task(func())
==》
tasks = [
asyncio.create_task(func(),name='任务1'),
asyncio.create_task(func(),name='任务2')
]
并且将
response1 = await task1
response2 = await task2
改写为
done, pending = await asyncio.wait(tasks, timeout=None)
例
import asyncio
async def func():
print("start")
await asyncio.sleep(1)
print("end")
return "这是func的返回值"
async def main():
print("main开始")
# 将任务放入到一个列表中
tasks = [
asyncio.create_task(func(),name='任务1'),
asyncio.create_task(func(),name='任务2')
]
print("main结束")
done, pending = await asyncio.wait(tasks, timeout=None)
print(done)
print('-'*20)
print(pending)
if __name__ == '__main__':
asyncio.run(main())
这里 await 后面不能跟一个列表,需要用 asyncio.wait 将它转换为一个协程对象,等待这个列表中的任务执行完毕
最终 await 返回一个包含(done, pending)的元组,done表示已完成的任务集合,集合元素是 tasks 中的所有任务的返回值,pending表示未完成的任务列表。
timeout是指过时时间,如果为 timeout=2 时,表示最多等2秒,如果2秒没有返回值,就不再接收返回值;如果为None时,表示一直等待返回值。
注:
- 只有当给wait()传入timeout参数时才有可能产生pending列表。
- 通过wait()返回的结果集是按照事件循环中的任务完成顺序排列的,所以其往往和原始任务顺序不同。
上述例子是把所有任务 tasks 写在了协程函数中,如果要将 tasks 写在协程函数外,可以这样写
import asyncio
async def func():
print("start")
await asyncio.sleep(1)
print("end")
return "这是func的返回值"
# 如果在这里直接添加任务则会报错,因为我们还未创建事件循环,所以无法添加任务
# 我们只需将协程对象放入列表中即可
# tasks = [
# asyncio.create_task(func()),
# asyncio.create_task(func())
# ]
tasks = [
func(),
func()
]
# 这里程序会自动为我们先创建事件循环,然后再创建任务
done, pending = asyncio.run(asyncio.wait(tasks))
print(done)
2.5 asyncio.Future对象
一般不会手动设置,仅供我们理解await的处理过程,实际中不会使用。
Future是一个更偏底层的接口,用于等待异步处理的结果。
await 等待结果本质是由 Future 创建的
Task继承Future,Task对象内部await结果的处理基于Future来的
import asyncio
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个任务(Future对象),这个任务什么都不干,不会有任何结果
fut = loop.create_future()
# 等待任务最终结果,没有结果则会一直等待下去
await fut
asyncio.run(main()) # asyncio.run已经创建好事件循环
Future对象通常用来让程序进入一个等待状态。Task对象是无法实现这个操作的,因为我们在创建Task对象时通常会绑定一个协程对象,而在执行任务即协程函数时,函数内没有返回值或没有内容都将自动返回一个None,使函数不再继续等待。(https://blog.csdn.net/weixin_46297209/article/details/111464854)
2.6 concurrent.futures.Future对象
concurrent.futures.Future对象 与 asyncio.Future对象 没有关系
使用场景:比如说我们在一个项目中导入的第三方包不支持async的异步编程时,就需要将协程与进程池、线程池结合使用。
3 案例 asyncio + 不支持异步的模块(爬虫,如requests)
上面说到
协程与进程池和线程池交叉使用
使用场景:比如说我们在一个项目中导入的第三方包不支持async的异步编程时,就需要将协程与进程池、线程池结合使用。
例如
requests模块就默认不支持异步操作(不支持async关键字),所以需要使用线程池来配合实现了,即使用异步与非异步结合的方式进行爬取
import asyncio
import requests
import os
os.chdir(r'C:\Users\Administrator\Desktop')
async def download_image(url):
print('开始下载。')
# 获取当前的事件循环
loop = asyncio.get_event_loop()
# requests模块默认不支持异步操作,可以配合线程池实现异步操作
# 会创建多个线程
# 创建一个线程池,将函数和参数扔进线程池中进行请求
# 参数None表示使用默认的线程池。
fut = loop.run_in_executor(None, requests.get, url)
# 此时遇到IO操作,不会等待,继续切换下一个任务,即将另一个请求扔进线程池中
resp = await fut
print('下载完成。')
# 保存
file_name = url.rsplit('/')[-1]
with open(file_name, mode='wb') as file_obj:
file_obj.write(resp.content)
if __name__ == '__main__':
url_list = [
'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2108399405,646348254&fm=26&gp=0.jpg',
'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1091405991,859863778&fm=26&gp=0.jpg',
'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3205720277,4209513487&fm=26&gp=0.jpg'
]
#创建任务列表
task_list = [download_image(each_url) for each_url in url_list]
# 获取当前的事件循环
#loop = asyncio.get_event_loop()
#loop.run_until_complete(asyncio.wait(task_list))
asyncio.run(asyncio.wait(task_list))
4 异步迭代器
异步迭代器:实现了方法__aiter__()和__anext__()的对象,aiter__必须返回一个awaitable对象,async for会处理异步迭代器的方法__anext()所返回的可等待对象,直到其引发了一个StopAsyncIteration异常。
5 异步上下文管理器
通过定义方法__aenter__()和__aexit__()对async with语句中的环境进行控制。
6 事件循环升级 uvloop
uvloop是asyncio中的事件循环的替代方案,大幅度提高事件循环效率和性能。
7 爬虫
在编写爬虫应用时,需要通过网络IO去请求目标数据,这种情况适合使用异步编程来提升性能,接下来我们使用支持异步编程的aiohttp模块来实现。
安装aiohttp模块
pip install aiohttp
import aiohttp
import asyncio
async def fetch(session, url):
print("发送请求:", url)
async with session.get(url, verify_ssl=False) as response:
text = await response.text()
print("得到结果:", url, len(text))
async def main():
async with aiohttp.ClientSession() as session:
url_list = [
'https://python.org',
'https://www.baidu.com',
]
tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
await asyncio.wait(tasks)
if __name__ == '__main__':
asyncio.run(main())
参考:
https://pythonav.com/wiki/detail/6/91/
从概念到案例全面剖析协程
2 异步编程 asyncio模块