在Python使用multiprocessing进行多线程和多进程操作 这篇文章中介绍了使用多线程的方式对一些I/O操作(文件读写、网络请求,这些操作不用等待其结束,在此期间可以做其他事情)进行加速。而本篇文章介绍的协程可以理解成“微线程”,不开辟其他线程,只在一个线程中执行,并且执行函数时可以被中断的一种异步执行功能。我们可以把采用线程实现的阻塞式I/O操作转化为采用协程实现地异步I/O操作。与多线程比较,没有切换线程的开销和多线程锁机制,因此使用协程可以更加高效实现异步操作。
1、async与await关键字
Python3.5之后我们可以采用async和await这两个关键字实现协程操作。它的基本原理与生成器(Python迭代器、生成器、装饰器的使用)类似,被asycn关键字修饰的函数,在函数被调用时不会立刻执行函数体内容,而是在等到需要时再一项一项的获取。协程可以从执行环境中获取输入值,并把这个函数执行后的结果放到这个执行环境中。协程与线程的区别在于,它不会把这个函数从头到尾的执行,而是遇到一个await表达式,就暂停一次,下次继续执行的时候,它会等待await所针对的那项操作(这个操作就是用async修饰的函数)有了结果,然后再推进到下一个await表达式那里。
Python可以让数量极多的async函数各自向前推进,看起来像很多条Python线程那样能够并发地运行。我们先看一个最简单的异步调用的例子:
import asyncio async def async_task(name): print(f"任务{name} 开始执行") await asyncio.sleep(1) print(f"任务{name} 执行结束") return f"返回结果{name}" result1 = async_task(1) result2 = async_task(2) result3 = async_task(3) print("result的类型: ", type(result1))
我们这里使用async定义了一个函数叫做async_task,这个函数传入一个参数name,函数体我们使用await asyncio.sleep(1) 模拟I/O堵塞1s的操作(注意这里不能使用time.sleep()函数来模拟,因为time.sleep()会将当前线程休眠并释放GIL,而对于协程来说我们只有一个线程,就是主线程,如果使用time.sleep()就是在堵塞主线程)。我们执行上述代码结果如下。
我们会发现函数体并没有被执行(函数体内部的print没有打印),而且我们函数的结果也是一个“coroutine”类型,并不是我们return返回值。与此同时系统还抛出了一个运行警告。这是因为上述代码其实只是在定义协程任务(执行async修饰的函数会得到一个coroutine对象),继续运行下面的代码,其实才是真正的开启了协程。
tasks = asyncio.wait([result1, result2, result3]) print("tasks类型: ", type(tasks)) asyncio.run(tasks)
这里的asyncio.wait()传入了一个coroutine对象的可迭代对象,asyncio.wait()会将这些任务进行打包整个生成一个任务并返回corountine对象(也就是tasks的类型)。最后一行asyncio.run()需要传入一个coroutine对象(可以直接传入result1,但不能直接传入[result1, result2, result3]因为这是一个list,所以我们需要先采用asyncio.wait()将这些任务打包),此时就会开启协程,运行函数体。我们看下运行结果:
我们可以看到此时才会真正的执行我们定义的函数体的内容(并且在开始执行和执行结束之间是有1s的停顿的,大家可以使用time库自行测试程序运行时间)。我们采用async和asyncio.run()完成了一个最简单的异步协程任务的使用。
2、Task和事件循环
在上面个例中我们已经可以简单的使用协程来完成异步操作了,但是我们会发现我们无法得到函数的返回结果。为了能得到任务函数的返回结果,我们还需要了解到协程更底层的一些概念和操作。首先我们要介绍两个概念:事件循环(Loop)、Task类。
- 事件循环(Loop):管理所有的任务,在整个程序运行过程中不断循环执行并追踪这些任务发生的顺序,并将它们放在执行队列中,空闲时调用相应的事件处理者来处理这些任务。可以看作将任务放在Loop中,那么会让CPU去执行这些任务,而Loop则起到一个管家和追踪者的作用。实际上是ProactorEventLoop类。
- Task类:这个类是asyncio.Task,它是asyncio.Future的子类。Future对象一般表示尚未完成的计算/结果,会在将来得到结果,这个对象一般不会直接创建,而是作为Task的基类。Task其实就是真正的运行对象,它可以看作是对协程函数的进一步封装,而Loop直接管理的也是Task对象。我们在上一节提到的“coroutine”类会在内部自动封装成Task类。
我们下面来介绍如何获得Loop对象和Task对象。
2.1、事件循环对象
我们可以通过以下方式获得事件循环对象:
- asyncio.get_running_loop():获取正在运行的事件循环对象(如果当前线程中没有正在运行的事件循环则会报错)
- asyncio.get_event_loop():获得一个事件循环对象,创建一个新的事件循环loop
- asyncio.set_event_loop(loop):为当前线程设置一个事件循环对象(该函数会返回一个事件循环对象)
- asyncio.new_event_loop():创建一个新的事件循环
我们得到一个Loop对象后,就可以事项Loop对象的方法来启动协程了(上一节启动协程的方式是使用asyncio.run()这是比较高层次的API,查看其源码可以知道其实内部也是获取了Loop对象,再使用Loop.run_until_complete()启动协程的),因此针对上一节的代码,我们还可以使用以下代码来启动异步协程任务。
loop = asyncio.get_event_loop() loop.run_until_complete(tasks)
loop.run_until_complete()这个函数从函数名就可以知道,含义是:启动事件循环,直到里面的Future全部完成(这里看成协程任务全部完成也是可以的)。这个函数是有返回值的,会根据你传入的参数(传入的参数必须是Task对象、Future对象、coroutine对象)而得到不同的返回值。如果你是传入的Task对象,那么你的返回值就是这个Task对象执行后的返回值,例如我们直接执行一个协程函数。
import asyncio async def async_task(name): print(f"任务{name} 开始执行") await asyncio.sleep(1) print(f"任务{name} 执行结束") return f"返回结果{name}" loop = asyncio.get_event_loop() result = loop.run_until_complete(async_task(1)) print(result)
最终的执行结果如下:
可以看到我们成功得到了协程函数的返回值(这也就解决了本节开头提出的问题:如何获取协程函数的返回值)。后面我们还会介绍如果loop.run_until_complete()传入的参数是其他对象那么所得到的返回值类型是不同的。
2.2、Task对象
我们常通过以下方式来得到一个Task对象:
- loop.create_task(corootine):从一个“coroutine”对象(被async修饰函数执行后就是一个coroutine)得到一个Task对象
- asyncio.ensure_future(coroutine):从一个“coroutine”对象得到一个Task对象(推荐使用)
可能有疑问为什么需要使用这些方法去得到Task对象呢,反正asyncio内部在执行任务时不就会将coroutine封装成Task执行么?其实是因为Task是最基本的可操作的协程对象(其实Future才是最基本的,不过我们一般都涉及不到这么底层,所以后文提到Future其实是指的其常用子类比如Task类)我们后续的一些操作比如给任务添加回调函数(callback),这都需要基于Task对象。并且我们还可以查看Task对象的当前状态(是否被执行了),以及执行后查看Task对象的返回值。下面我们通过手动将coroutine封装成Task对象,并使用Loop执行任务,看看会发生什么。
import asyncio async def async_task(name): print(f"任务{name} 开始执行") await asyncio.sleep(1) print(f"任务{name} 执行结束") return f"返回结果{name}" loop = asyncio.get_event_loop() task = loop.create_task(async_task(1)) print("类型:", type(task)) print('任务运行前状态: ', task) loop.run_until_complete(task) # 运行task对象 print('\n任务运行后状态: ', task) print("任务返回值: ", task.result())
我们在代码第十行通过loop.create_task()将一个执行后的协程函数(得到coroutine对象)转化为了一个Task类对象,并且在执行任务前和任务执行后都分别打印了task。需要注意的是我们在代码第13行并没有接收loop.run_until_complete的结果,取而代之的是我们在代码的最后一行直接调用task.result()返回了这个任务的返回值(也就是函数return的那个字符串)。运行后结果如下:
我们可以看到运行前的task和运行后的task有着明显的不同,运行后的task具有result这个内容并且还有finished标识,而运行前则是pending标识。并且我们最后也成功的使用task.result()得到了任务的返回值。
标签:task,协程,Python,对象,Task,loop,asyncio From: https://www.cnblogs.com/CircleWang/p/17390920.html