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 greenletdef 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()
输出
1324
1.2 yield关键字
- 示例:
def func1():yield 1yield from func2()yield 2def func2():yield 3yield 4f1 = func1()for item in f1:print(item)
输出
1342
1.3 asyncio
- 在python3.4及以后的版本
- 示例:
import asynciodef 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阻塞自动切换
输出
1324
1.4 async & await关键字
- 在py3.5之后的版本
- 示例:
import asyncioasync 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))
输出
1324
2. 协程的意义
- 在一个线程中如果遇到一个IO等待时间,线程不会傻傻等,利用空闲时间再去干点其他事
- 案例:去下载三张图片(IO)
- 普通方式
import requestsimport timedef 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_timeprint("下载时间为:", 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 aiohttpimport asyncioimport timeasync 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_timeprint("下载时间为:", 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 asyncioasync def func():passresult = func() # result就是一个协程对象注意:执行协程函数创建协程对象,函数内部代码不会执行如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理
- 示例:
import asyncioasync 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 asyncioasync def func():print("你好呀!")response = await asyncio.sleep(2)print("结束",response)asyncio.run(func())
输出
你好呀!结束 None
- 示例2:
import asyncioasync 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())
输出
执行协程函数内部代码startendIO请求结束,结果为:返回值
- 示例3:(可以接多个await)
import asyncioasync 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就是等待对象的值得到结果之后再继续往下走
输出
执行协程函数内部代码startendIO请求结束,结果为:返回值startendIO请求结束,结果为:返回值
3.4 Task对象
- 白话:在事件循环中添加多个任务
- 注意:asyncio.create_task(协程对象)函数在python3.7中被加入,3.7之前可以用低层级的asyncio.ensure_future()函数
- 示例1:
import asyncioimport timeasync 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 task1ret2 = await task2print(ret1,ret2)end = time.time()print(end - start)asyncio.run(main())
执行结果为
main开始main结束1122返回值 返回值2.0011146068573
- 示例2:常用写法
#上面代码的改进写法,常用写法import asyncioasync 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结束1122{<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 asyncioasync 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 asyncioasync 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)
输出结果为
1122{<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 futasyncio.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 futprint(data)asyncio.run(main())
输出结果为
666
