python中的异步io——asyncio库的使用(二)

异步必要性

在编程中,我们免不了接触IO处理,这其中就涉及到两种IO方式,分别是同步IO和异步IO,下面我们将用1G文件的存储来描述同步IO和异步IO分别是什么

同步: 1G的文件写入磁盘,在写入的这段时间CPU一直被占用,无法进行其他操作

异步: 1G的文件写入磁盘,再写入的这段时间CPU去做别的事,磁盘的写入放着让他写

IO: 就是对应input数据流入磁盘;output从磁盘读取数据

为什么要有异步IO?

因为CPU和内存的速度远远高于外设的速度(外设就是除主机外的大部分硬件设备都可称作外部设备,或叫外围设备,如磁盘、显示器、鼠标键盘等等),因此在IO编程中,就存在速度严重不匹配的问题,还是以上面的文件存储为例子,我们可以选择两种方案:①花1min进行文件存储,等到存储结束后CPU继续工作 ②这1min让他自己去操作,CPU继续处理其他事

如果我们采用①IO就会一直占用这CPU,电脑就会产生卡住的现象!

而使用②方案,这样可以减少CPU的占用,CPU能够更大程度的被使用!

同步IO和异步IO有什么好处?

异步IO来编写程序性能会远远高于同步IO,但是异步IO的编程模型复杂。
举个例子:我们从小听到大的,烧水的时候我们可以去做别的事情!这就是异步! 假如说我们烧完水还要用这些热水去泡泡面!那我们就得知道水什么时候烧开,如果是听到水壶的响声然后我们就回来接着泡泡面,这就是回调模式
如果水壶没声音!那我们就得每隔一段时间就来看看水烧开了没有,这就轮询模式。异步IO的复杂度远远高于同步IO。

什么是协程
协程并非计算机提供的,而是程序员人为创造的,可以把它理解为微线程,是一种上下文切换技术。
通俗点讲,他就是一种能够实现代码块之间相互切换的技术。

asyncio的使用
Asyncio 遇到IO 自动切换! 不需要像yield那样手动切换
四个重要概念: 1、Eventloop 2、Coroutine 3、Future 4、Task

1、Eventloop
Eventloop提供了注册、取消和执行任务和回调的方法,我们可以把异步任务塞到 Eventloop 中,Eventloop一次只会执行一个函数,当执行的函数遇到IO事件时,就暂停这个函数的运行,去执行其他Eventloop中的函数,等到下次循环到他的时候再继续执行!

2、Coroutine
协程的本质是函数,但是协程可以在运行的时候将执行权交给其他函数。
看下面例子:

currTime = time.time()

async def fun1():
    print('func1 first run , time ', time.time()-currTime)
    await asyncio.sleep(1)
    print('func1 second run , time ', time.time()-currTime)


async def fun2():
    print('func2 first run , time ', time.time()-currTime)
    await asyncio.sleep(1)
    print('func2 second run , time ', time.time()-currTime)


async def mainFunc():
    await asyncio.gather(fun1(), fun2())


asyncio.run(mainFunc())

运行结果如下:

func1 first run , time  0.0020003318786621094
func2 first run , time  0.0020003318786621094
func1 second run , time  1.012211799621582
func2 second run , time  1.012211799621582

上述例子中,我们使用asyncio.gather()来并发两个协程任务,用sleep来模拟IO事件,可以看出当遇到await的时候,就把控制权交出去
从运行结果的输出时间也可以看出,两个函数的的第一个和第二个print是分别相同时间的,证明并没有去等待时间而是直接去运行另一个函数

Tips:

asyncio.run(mainFunc())
# 等价于如下三行代码 具体原因可以去查看run函数
loop = asyncio.get_event_loop()
loop.run_until_complete(mainFunc())
loop.close()

3、Future
在我们异步操作结束后,会把最终结果设置到Future对象上,因此Future就是对协程的封装,但Future只是一个底层基类,正常情况下我们是会直接使用其子类Task(后面就会讲到)
Future含有三种状态,如下分别是 挂起、取消、完成
在这里插入图片描述
4、Task
Task是Future的子类,Task是对协程的封装,我们将多个Task放到一个循环调度的列表中,等待被调度执行
Task对象来完成任务的并和状态的追踪。( Task 是 Futrue的子类 )Future为我们提供了异步编程中的 最终结果 的处理(Task类也具备状态处理的功能)。
我们把协程封装成Task,并将其加入到一个队列中等待调用,刚创建Task的时候并不执行,而是当遇到await的时候才会被执行

import asyncio

async def func():
	print(1)
	await asyncio.sleep(2)
	print(2)
	return "返回值"


async def main():
	print("main开始")

	# 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
	# 在调用
	task_list = [
	asyncio.create_task(func(), name="n1"),
	asyncio.create_task(func(), name="n2")
	]

	print("main结束")

	# 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
	# 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
	# 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
	done, pending = await asyncio.wait(task_list, timeout=None)
	print(done, pending)


asyncio.run(main())

运行结果如下:

main开始
main结束
1
1
2
2
{
<Task finished name='n1' coro=<func() done, defined at F:/PythonXSLWorkSpace/PythonBaseUse/AsyncioUse/AsyncioTest.py:32> result='返回值'>, 
<Task finished name='n2' coro=<func() done, defined at F:/PythonXSLWorkSpace/PythonBaseUse/AsyncioUse/AsyncioTest.py:32> result='返回值'>
} 
 set()
返回值
返回值

可以看到,输出的done、pending 分别表示 **已完成队列、悬挂队列(未完成)**两个队列内的内容分别是:两个Task和一个空集合,因为我们将timeout设置为None,此时会将里面的所有task运行完毕才会结束

此时我们将上述timeout改为1,即 timeout=1,此时运行结果如下:

main开始
main结束
1
1
set() 
 {
 <Task pending name='n1' coro=<func() running at F:/PythonXSLWorkSpace/PythonBaseUse/AsyncioUse/AsyncioTest.py:34> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002B6BBE7BB20>()]>>, 
 <Task pending name='n2' coro=<func() running at F:/PythonXSLWorkSpace/PythonBaseUse/AsyncioUse/AsyncioTest.py:34> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002B6BBE7BB50>()]>>
 }

可以看到,输出的done、pending 分别是 一个空集合和两个Task,因为我们将timeout设置为1,此时两个task均还处于IO状态(用asyncio.sleep(2))表示IO。

注意下面的使用和上面使用的不同,此时我们并没有将 TaskList的调用封装在一个异步函数中

import asyncio

async def func():
	print("执行协程函数内部代码")
	# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
	response = await asyncio.sleep(2)
	print("IO请求结束,结果为:", response)

coroutine_list = [func(), func()]
	# 错误:coroutine_list = [ asyncio.create_task(func()), asyncio.create_task(func()) ] 
	# 此处不能直接 asyncio.create_task,因为将Task立即加入到事件循环的任务列表,
	# 但此时事件循环还未创建,所以会报错。

	# 使用asyncio.wait将列表封装为一个协程,并调用asyncio.run实现执行两个协程
	# asyncio.wait内部会对列表中的每个协程执行ensure_future,封装为Task对象。
done,pending = asyncio.run( asyncio.wait(coroutine_list) )

三、asyncio在实际使用中要注意的点

看下面这段代码

currTime = time.time()


async def fun1():
    print('func1 first run , time ', time.time()-currTime)
    await asyncio.sleep(1)
    print('func1 second run , time ', time.time()-currTime)


async def fun2():
    print('func2 first run , time ', time.time()-currTime)
    await asyncio.sleep(3)
    print('func2 second run , time ', time.time()-currTime)


async def mainFunc():
	# 方式一
    await fun1()
    await fun2()
	# 方式二
    # await asyncio.gather(fun1(), fun2())

asyncio.run(mainFunc())

当我们采用方式一输出的结果如下:

func1 first run , time  0.0009996891021728516
func1 second run , time  1.0158805847167969
func2 first run , time  1.0158805847167969
func2 second run , time  4.029984951019287

当我们采用方式二输出的结果如下:

func1 first run , time  0.0020008087158203125
func2 first run , time  0.0020008087158203125
func1 second run , time  1.0107042789459229
func2 second run , time  3.025214910507202

当采用方式一的时候可以发现最后一次打印的时间是4s左右
当采用方式一的时候可以发现最后一次打印的时间是3s左右
从输出结果可以看出,当使用**await asyncio.gather(fun1(), fun2())**的时候,调用顺序才是真正的异步IO,多个协程在运行的时候,当第一个协程遇到IO的时候,gather将控制权交了出去,而第一种方式并没有将控制权交出去而是等待IO结束

四、简单描述一下asyncio在使用过程中的调用栈

我们从最简单的例子说明

async def a():
    print("func a running")
    return "a func"
loop = asyncio.get_event_loop()
task = loop.create_task(a())
async def testFuture():
    a = await task
result = loop.run_until_complete(testFuture())

1、python环境在运行的时候会先判断当前平台, 在asyncio模块的__init__.py文件中,可以看到如下代码
在这里插入图片描述

因此在windows平台下,导入的库是windows_events类库
2、loop = asyncio.get_event_loop() 我们通过这个方法获取事件循环的方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在windows_events.py 和unix_events.py 中有分别如下定义,这便是初始化的时候要区分平台导入库的原因
在这里插入图片描述
在这里插入图片描述

下面我们以Windows平台为例讲述

在这里插入图片描述
在这里插入图片描述

从上一张图可以看到,这个**_loop_factory** 就是ProactorEventLoop
在这里插入图片描述

重点来看这一句里面的实现proactor = IocpProactor()
关于什么是IOCP 查看该文章什么是IOCP
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、如果我们还想提高异步的速度,此时该怎么办?

我们可以使用uvloop来加快异步的速度,数据证明,uvloop可以达到原来速度2倍

import asyncio
import uvloop
# 声明使用 uvloop 事件循环
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)

其余的调用方法和asyncio的一致

后面得闲会比较一下uvloop和自带的eventloop有何区别

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值