3-3,协程&asyncio&异步编程补充
协程
- 不是计算机提供,程序员人为创造
- 协程(coroutine),也可以成为微线程,是一种用户态内的上下文切换技术。
- 简而言之,其实就是通过一个线程实现代码块相互切换执行。例如
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()
-实现协程有这么几种方法:
-grenlet,早期模块
-yield关键字
-asyncio装饰器(py3.4)
-async、await关键字(py3.5)【推荐】
1.1 greenlet实现协程
- 示例:
from greenlet import greenlet
def func1():
print(1)
gr2.switch()
print(2)
gr2.switch()
def func2():
print(3)
gr1.switch()
print(4)
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()
输出
1
3
2
4
1.2 yield关键字
- 示例:
def func1():
yield 1
yield from func2()
yield 2
def func2():
yield 3
yield 4
f1 = func1()
for item in f1:
print(item)
输出
1
3
4
2
1.3 asyncio
- 在python3.4及以后的版本
- 示例:
import asyncio
def func1():
print("1")
# 网络IO请求 下载一张图片
await asyncio.sleep(2)
print("2")
def func2():
print("3")
# 网络IO请求 下载一张图片
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
3
2
4
1.4 async & await关键字
- 在py3.5之后的版本
- 示例:
import asyncio
async def func1():
print("1")
# 网络IO请求 下载一张图片
await asyncio.sleep(2)
print("2")
async def func2():
print("3")
# 网络IO请求 下载一张图片
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))
输出
1
3
2
4
2. 协程的意义
- 在一个线程中如果遇到一个IO等待时间,线程不会傻傻等,利用空闲时间再去干点其他事
- 案例:去下载三张图片(IO)
- 普通方式
import requests
import time
def download_img(url):
print("开始下载:",url)
response = requests.get(url)
print("下载完成")
filename = url.split('/')[-1]
with open(filename,'wb') as fp:
fp.write(response.content)
if __name__ == "__main__":
start_time = time.time()
url_list = [
'http://pic.netbian.com/uploads/allimg/200629/230511-1593443111e579.jpg',
'http://pic.netbian.com/uploads/allimg/200411/212620-1586611580283a.jpg',
'http://pic.netbian.com/uploads/allimg/190925/201558-15694137581dc0.jpg'
]
for item in url_list:
download_img(item)
end_time = time.time()
t = end_time - start_time
print("下载时间为:", t)
输出:
开始下载:http://pic.netbian.com/uploads/allimg/200629/230511-1593443111e579.jpg
下载完成
开始下载:http://pic.netbian.com/uploads/allimg/200411/212620-1586611580283a.jpg
下载完成
开始下载:http://pic.netbian.com/uploads/allimg/190925/201558-15694137581dc0.jpg
下载完成
下载时间为:1.0819127559661865
- 协程方式:
import aiohttp
import asyncio
import time
async def fetch(session,url):
print("发送请求",url)
async with session.get(url,verify_ssl=False) as response:
content = await response.content.read()
file_name = url.split("/")[-1]
with open(file_name, 'wb') as fp:
fp.write(content)
async def main():
async with aiohttp.ClientSession() as session:
start_time = time.time()
url_list = [
'http://pic.netbian.com/uploads/allimg/200629/230511-1593443111e579.jpg',
'http://pic.netbian.com/uploads/allimg/200411/212620-1586611580283a.jpg',
'http://pic.netbian.com/uploads/allimg/190925/201558-15694137581dc0.jpg'
]
tasks = [asyncio.create_task(fetch(session,url)) for url in url_list]
await asyncio.wait(tasks)
end_time = time.time()
t = end_time - start_time
print("下载时间为:", t)
if __name__ == "__main__":
asyncio.run(main())
print("下载完成")
输出
发送请求 http://pic.netbian.com/uploads/allimg/200629/230511-1593443111e579.jpg
发送请求 http://pic.netbian.com/uploads/allimg/200411/212620-1586611580283a.jpg
发送请求 http://pic.netbian.com/uploads/allimg/190925/201558-15694137581dc0.jpg
下载时间为:0.7550625801086426
下载完成
3.异步编程
3.1 事件循环
- 理解为一个死循环,去检测并执行某些代码
- 伪代码
任务列表 = [任务1,任务2,任务3.。。]
while True:
可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将可执行和已完成的任务返回
for 就绪任务 in 已就绪的任务列表:
执行已就绪的任务
for 已完成任务 in 已完成的任务列表:
在任务列表中移除
已完成的任务
如果任务列表中的任务已完成 终止循环
import asyncio
# 去生成一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到任务列表
loop.run_until_complete(asyncio.wait(tasks))
3.2 快速上手
- 协程函数,定义函数时,asyncio def 函数名。
- 协程对象,执行协程函数()得到的协程对象。
import asyncio
async def func():
pass
result = func() # result就是一个协程对象
注意:执行协程函数创建协程对象,函数内部代码不会执行
如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理
- 示例:
import asyncio
async def func():
print("你好呀")
result = func()
# loop = asyncio.get_event_loop()
# loop.run_until_complete(result)
asyncio.run(result)#py3.7之后
输出
你好
3.3 await
- await+可等待的对象(协程对象、Future、Tsak对象->IO等待)
- 示例1:
import asyncio
async def func():
print("你好呀!")
response = await asyncio.sleep(2)
print("结束",response)
asyncio.run(func())
输出
你好呀!
结束 None
- 示例2:
import asyncio
async def others():
print("start")
await asyncio.sleep(2)
print('end')
return '返回值'
async def func():
print("执行协程函数内部代码")
# 遇到IO操作挂起当前协程,等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)
response = await others()
print("IO请求结束,结果为:",response)
asyncio.run(func())
输出
执行协程函数内部代码
start
end
IO请求结束,结果为:返回值
- 示例3:(可以接多个await)
import asyncio
async def others():
print("start")
await asyncio.sleep(2)
print('end')
return '返回值'
async def func():
print("执行协程函数内部代码")
response1 = await others()
print("IO请求结束,结果为:",response1)
response2 = await others()
print("IO请求结束,结果为:", response2)
asyncio.run(func())
- await就是等待对象的值得到结果之后再继续往下走
输出
执行协程函数内部代码
start
end
IO请求结束,结果为:返回值
start
end
IO请求结束,结果为:返回值
3.4 Task对象
- 白话:在事件循环中添加多个任务
- 注意:asyncio.create_task(协程对象)函数在python3.7中被加入,3.7之前可以用低层级的asyncio.ensure_future()函数
- 示例1:
import asyncio
import time
async def func():
print("1")
await asyncio.sleep(2)
print('2')
return '返回值'
async def main():
start = time.time()
print("main开始")
#创建task对象,将当前执行func函数任务添加到事件循环
task1 = asyncio.create_task(func())
task2 = asyncio.create_task(func())
print("main结束")
ret1 = await task1
ret2 = await task2
print(ret1,ret2)
end = time.time()
print(end - start)
asyncio.run(main())
执行结果为
main开始
main结束
1
1
2
2
返回值 返回值
2.0011146068573
- 示例2:常用写法
#上面代码的改进写法,常用写法
import asyncio
async def func():
print("1")
await asyncio.sleep(2)
print('2')
return '返回值'
async def main():
print("main开始")
task_list = [
asyncio.create_task(func(),name="n1"),
asyncio.create_task(func(),name="n2")
]
print("main结束")
# await 后不能直接加task_list,将多个task对象加到协程中
# done:已完成的任务返回值都会返回到done中,是个集合,元素即为返回值
# pending即为还没完成的任务
done,pending = await asyncio.wait(task_list,timeout=None)
print(done)
asyncio.run(main())
执行结果为
main开始
main结束
1
1
2
2
{<Task finished name='n2' coro=<func() done, defined at E:/pywork/小猿圈爬虫课程(2020爬虫全套教程)/协程&asyncio&异步/test.py:201> result='返回值'>, <Task finished name='n1' coro=<func() done, defined at E:/pywork/小猿圈爬虫课程(2020爬虫全套教程)/协程&asyncio&异步/test.py:201> result='返回值'>}
- 示例3:(task_list在asyncio之前)
import asyncio
async def func():
print("1")
await asyncio.sleep(2)
print('2')
return '返回值'
task_list = [
asyncio.create_task(func(),name="n1"),
asyncio.create_task(func(),name="n2")
]
done,pending = asyncio.run(asyncio.wait(task_list))
print(done)
# 会报错,在task_list位置未创建事件循环
应改成
import asyncio
async def func():
print("1")
await asyncio.sleep(2)
print('2')
return '返回值'
#写在外边不应设置成Task对象,未有事件循环
task_list = [
func(),
func()
]
# asyncio.run内部会创建事件循环
done,pending = asyncio.run(asyncio.wait(task_list))
print(done)
输出结果为
1
1
2
2
{<Task finished name='Task-3' coro=<func() done, defined at E:/pywork/小猿圈爬虫课程(2020爬虫全套教程)/协程&asyncio&异步/test.py:229> result='返回值'>, <Task finished name='Task-2' coro=<func() done, defined at E:/pywork/小猿圈爬虫课程(2020爬虫全套教程)/协程&asyncio&异步/test.py:229> result='返回值'>}
- task对象就是将某个任务放到事件循环,并发的创建多个任务,让事件循环遇到IO自由切换处理。
3.5 asyncio.Future对象(更底层,不常用,要理解)
- Task继承Future,Task对象内部await结果的处理基于Future对象来的
- 示例1:
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
#创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候结束
fut = loop.create_future()
#等待任务最终结果(Future对象),没有结果则会一直等待下去
await fut
asyncio.run(main())
- 示例2:
async def set_affer(fut):
await asyncio.sleep(2)
fut.set_result("666")
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候结束
fut = loop.create_future()
#创建一个任务(Task对象),绑定了set_after函数,函数内部在2s以后,会给fut赋值
#即手动设置future任务的最终结果,那么fut就可以结束了
await loop.create_task(set_affer(fut))
#等待Future对象获取最终结果,否则会一直等待下去
data = await fut
print(data)
asyncio.run(main())
输出结果为
666