当前位置: 首页 > 后端技术 > Python

多任务(三):协程

时间:2023-03-26 16:31:20 Python

代码环境:python3.6上一篇讲了多线程在python中的使用:点击阅读,现在讲讲python中的协程。异步IO我们知道CPU的速度比磁盘、网络等IO要快很多。在IO编程中,如果一个IO操作阻塞了当前线程,其他代码就无法执行,所以我们使用多个线程或进程并发执行代码。但是,系统资源是有限的。一旦线程过多,CPU时间就会耗费在线程切换上,实际执行代码的时间就会减少,导致性能严重下降。针对这个问题,我们需要另一种解决方案:异步IO。异步IO,即当代码需要执行一个耗时的IO操作时,只发出IO指令,不等待IO结果,再执行其他代码。一段时间后,当IO返回结果时,会通知CPU进行处理。python中的原始协程了解原始协程有助于我们后面理解现代协程的用法。协程的概念并不是python首先提出的,而是借鉴了其他语言。我们知道,两个普通函数的调用是有顺序的。比如函数A调用函数B,B执行完后返回结果给A,A执行完。协程似乎也是函数。如果协程A调用协程B,在执行过程中,可以中断协程B,然后执行A,然后在适当的时候返回到B,然后从中断点开始执行。协程的这个执行特性正好满足了我们的需求:通过协程进行异步IO编程。生成器演变成协程。Python是在生成器的基础上进行了一系列的功能改进,得到一个协程。从句法上讲,定义主体包含yield关键字。在协程中,yield不仅可以返回一个值,还可以接收调用者通过.send()方法发送的参数。yield通常出现在表达式的右侧,如:data=yieldsomething。如果yield后面没有表达式,说明此时yield只负责接收数据,协程一直返回None。简单协程的基本行为举个简单的例子:In[1]:defmy_coroutine():...:print('coroutineisactivated')...:whileTrue:#Yield后面没有表达式,这里只接收send()传过来的数据...:x=yield...:print(f'coroutinereceivedparameters:{x}')...:In[2]:my_corou=my_coroutine()#你可以查看协程当前状态In[3]:frominspectimportgetgeneratorstateIn[4]:getgeneratorstate(my_corou)Out[4]:'GEN_CREATED'#激活协程,这里可以使用my_corou.send(None)而不是In[5]:next(my_corou)coroutineisactivatedIn[6]:getgeneratorstate(my_corou)Out[6]:'GEN_SUSPENDED'In[7]:return_value=my_corou.send(99)coroutinereceivedparameters:99In[8]:print(return_value)NoneIn[9]:my_corou.close()In[10]:getgeneratorstate(my_corou)Out[10]:'GEN_CLOSED'通过例子,我们主要理解的是需要激活协程手动调用之前。记得在不需要时将其关闭。使用协程改进生产者-消费者模型。在传统的生产者-消费者模型中,一个线程写消息,另一个线程取消息。队列和等待是通过锁机制来控制的,但是一不小心就可能出现死锁。如果换成协程,producer产生消息后,直接通过yield跳转到consumer开始执行,consumer执行完再切换回producer继续生产。整个过程无锁高效:frominspectimportgetgeneratorstatedefconsumer():r='200OK'whileTrue:#yield接收生产者的数据赋值给n,返回处理结果状态r给n=yieldrprint(f'[CONSUMER]consumed:{n}')defproducer(c):#不要忘记激活协程c.send(None)n=0whilen<5:n=n+1print(f'[PRODUCER]produced:{n}')#一旦produced拿到东西后,通过c.send()切换到consumer去执行#consumer处理完数据后,通过yield返回结果状态,这里为获取返回内容r=c.send(n)print(f'[PRODUCER]消费者返回的处理结果:{r}')print(f'生产者不再生产,查看当前消费者状态:{getgeneratorstate(c)}')c.close()print(f'关闭消费者,查看当前消费者状态:{getgeneratorstate(c)}')if__name__=="__main__":producer(consumer())在上面的例子中,整个过程ess只有一个线程执行,没有锁。生产者和消费者合作完成任务。这是一种协作式多任务处理。应该区分多线程的抢占式多任务。asyncio在python3.4版本开始引入标准库asyncio,直接内置了对异步IO的支持。asyncio的编程模型是一个消息循环。我们直接从asyncio模块中获取EventLoop的引用,然后将需要执行的协程丢到EventLoop中执行,从而实现了异步IO。下面简单介绍一下asyncio涉及的一些词:Future:表示异步执行的操作的对象。一般情况下,Future不应该自己创建,只能通过asyncio等并发框架来实例化。Task:负责执行EventLoop中协程的任务,是Future的子类。换句话说,Task是Future,但不一定相反。以下是asyncio的常用API:asyncio.get_event_loop():获取EventLoop对象运行协程asyncio.iscoroutine(obj):判断对象是否为协程。asyncio.sleep(delay):可以直接看成是多少秒的协程。asyncio.ensure_future(coro_or_future):如果入参是协程,则启动协程,返回一个Task对象;如果入参是Future,则直接返回入参。asyncio.gather(coros_or_futures):按照输入参数中协程的顺序保存协程的执行结果,大多数情况下使用。asyncio.wait(futures):与gather相比,不一定按照输入参数的顺序返回执行结果。返回的任务包括已完成和待处理的任务。可以通过接收参数return_when来选择返回结果的时机,根据实际情况使用。我们将在下面使用新关键字async/await作为示例。async/await为了简化使用和识别异步IO,从python3.5开始引入了一个新的语法糖async/await,使用async将一个生成器标记为协程函数,然后使用await调用里面的另一个协程实现coroutine异步操作。注意:协程函数标有async。调用函数时协程还没有被激活。该函数可以通过await或yieldfrom激活,也可以通过ensuring_future()或AbstractEventLoop.create_task()调度执行。例如:fromasyncioimportsleepasaiosleep,gather,get_event_loopasyncdefcompute(x,y):print("Compute%s+%s..."%(x,y))awaitaiosleep(1)returnx+yasyncdefprint_sum(x,y):result=awaitcompute(x,y)print("%s+%s=%s"%(x,y,result))asyncdefcoro_main():'''通常我们会编写协程main函数,负责管理协程'''awaitgather(print_sum(1,2),print_sum(4,9))defmain():aioloop=get_event_loop()#内部使用ensure_future()激活coroutineaioloop.run_until_complete(coro_main())aioloop.close()if__name__=="__main__":main()执行结果:计算1+2...计算4+9...(停顿1秒左右,实际输出没有这一行)1+2=34+9=13观察例子的运行结果,我们可以看到在协程开始计算1+2之前,有一个IO操作耗时1秒,当前thread没有在等待,而是去执行其他协程来calculate4+9,实现并发执行。协程的结果按照gather输入参数的顺序打印。小结面对CPU的高速执行和IO设备的低速严重不匹配,我们至少需要知道两种解决方案:使用多进程、多线程并发执行代码;使用异步IO来执行代码。python协程基于generator的改进,底层实现在定义体中始终包含yield关键字。协程属于协作式多任务,整个过程不需要锁,这一点必须与多线程等抢占式多任务区分开来。asyncio支持异步IO。我们直接从asyncio模块中获取EventLoop的引用,然后将需要执行的协程丢到EventLoop中执行,从而实现了异步IO。在定义协程函数时,我们将协程函数标记为async,然后在协程内部使用await调用另一个协程来实现异步操作。