Task和Future 前面我们讨论了协程,以及如何在循环中运行它们才有用。现在我想简单谈谈Task和Futureapi。你将使用最多的是Task,因为你的大部分工作将涉及使用createtask()函数运行协程,就像在第22页的快速开始中设置的那样。Future类实际上是Task的超类,它提供了与循环交互操作的所有功能。 可以这样简单地理解:Future表示某个活动的未来完成状态,并由循环管理。Task是完全相同的,但是具体的activity是一个协程可能是你用asyncdef函数加上createtask()创建的协程。 Future类表示与循环交互的某个东西的状态。这个描述太模糊了,不太有用,所以你可以将Future实例视为一个切换器,一个完成状态的切换器。当创建Future实例时,切换设置为尚未完成状态,但稍后它将是完成状态。事实上,Future实例有一个名为done()的方法,它允许你检查状态,如示例315所示。 示例315。用done()方法检查完成状态fromasyncioimportFuturefFuture()f。done()False Future实例还可以执行以下操作: 设置一个result值(用。setresult(value)设置值并且使用。result()获取值) 使用。cancel()方法取消(并且会用使用。cancelled()检查是否取消) 增加一个Future完成时回调的函数 即使Task更常见,也不可能完全避免使用Future:例如,在执行器上运行函数将返回Future实例,而不是Task。让我们快速看一下示例316,了解一下直接使用Future实例是什么感觉。 示例316。与Future实例的交互importasyncioasyncdefmain(f:asyncio。Future):。。。awaitasyncio。sleep(1)。。。f。setresult(Ihavefinished。)。。。loopasyncio。geteventloop()futasyncio。Future()print(fut。done())Falseloop。createtask(main(fut))TaskpendingnameTask1coromain()runningatconsole:1loop。rununtilcomplete(fut)Ihavefinished。print(fut。done())Trueprint(fut。result())Ihavefinished。 (L3)创建一个简单的main函数。我们运行这个函数,等上一会儿然后在Futuref上设置一个结果。 (L5)设置一个结果。 (L8)手动创建一个Future实例。注意,这个实例(默认情况下)绑定到我们的循环,但它没有也不会被附加到任何协程(这就是Tasks的作用)。 (L9)在做任何事情之前,确认future还没有完成。 (L11)安排main()协程,传递future。请记住,main()协程所做的所有工作就是sleep,然后切换Future实例。(注意main()协程还不会开始运行:协程只在事件循环运行时才开始运行。) (L13)在这里我们在Future实例上而不是Task实例上使用rununtilcomplete()。这和你以前见过的不一样。现在循环正在运行,main()协程将开始执行。 (L16)最终,当future的结果被设置时,它就完成了。完成后,可以访问结果。 当然,你不太可能以这里所示的方式直接使用Future;代码示例仅用于教育目的。你与asynccio的大部分联系都是通过Task实例进行的。 你可能想知道如果在Task实例上调用setresult()会发生什么。在Python3。8之前可以这样做,但现在不允许这么做了。任务实例是协程对象的包装器,它们的结果值只能在内部设置为底层协程函数的结果,如示例317所示那样。 示例317。在task上调用setresultimportasynciofromcontextlibimportsuppressasyncdefmain(f:asyncio。Future):。。。awaitasyncio。sleep(1)。。。try:。。。f。setresult(Ihavefinished。)。。。exceptRuntimeErrorase:。。。print(fNolongerallowed:{e})。。。f。cancel()。。。loopasyncio。geteventloop()futasyncio。Task(asyncio。sleep(1000000))print(fut。done())Falseloop。createtask(main(fut))TaskpendingnameTask2coromain()runningatconsole:1withsuppress(asyncio。CancelledError):。。。loop。rununtilcomplete(fut)。。。Nolongerallowed:Taskdoesnotsupportsetresultoperationprint(fut。done())Trueprint(fut。cancelled())True (L13)唯一的区别是我们创建的是Task实例而不是Future实例。当然,TaskAPI要求我们提供一个协程;这里我们使用sleep()只是因为简单方便。 (L7)正在传入一个Task实例。它满足函数的类型签名(因为Task是Future的子类),但从Python3。8开始,我们不再允许在Task上调用setresult():尝试这样做将引发RuntimeError。这个想法是,一个Task代表一个正在运行的协程,所以结果应该总是来自于task自身。 (L10,L24)但是,我们仍然可以cancel()一个任务,它将在底层协程中引发CancelledError。 Createtask?EnsureFuture?下定决心吧! 在第22页的快速入门中,我说过运行协程的方法是使用asyncio。createtask()。在引入该函数之前,有必要获取一个循环实例并使用loop。createtask()完成相同的任务。事实上,这也可以通过一个不同的模块级函数来实现:asyncio。ensurefuture()。一些开发人员推荐createtask(),而其他人推荐ensurefuture()。 在我为这本书做研究的过程中,我确信API方法asyncio。ensurefuture()是引起对asyncio库广泛误解的罪魁祸首。API的大部分内容都非常清晰,但在学习过程中还存在一些严重的障碍,这就是其中之一。当你遇到ensurefuture()时,你的大脑会非常努力地将其集成到关于asyncio应该如何使用的心理模型中但很可能会失败! 在Python3。6asyncio文档中,这个现在已经臭名昭著的解释突出了ensurefuture()的问题: asyncio。ensurefuture(coroorfuture,,loopNone) 安排执行一个协程对象:把它包装在future中。返回一个Task对象。如果参数是Future,则直接返回。 什么!?当我第一次读到这篇文章时,我很困惑。下面希望是对ensurefuture()的更清楚的描述:如果你传入一个协程,它将生成一个Task实例(你的协程将被调度在事件循环上运行)。这与调用asyncio。createtask()(或loop。createtask())并返回新的Task实例是相同的。如果你传入一个Future实例(或者一个Task实例,因为Task是Future的子类),你会得到相同的东西返回,没有改变。是的,真的! 这个函数很好地说明了针对终端用户开发人员的asyncioAPI(高级API)和针对框架设计人员的asyncioAPI(低级API)之间的区别。让我们在示例318中自习看看它是如何工作的。 示例318。仔细看看ensurefuture()在做什么importasyncioasyncdeff():passcorof()loopasyncio。geteventloop()taskloop。createtask(coro)assertisinstance(task,asyncio。Task)newtaskasyncio。ensurefuture(coro)assertisinstance(newtask,asyncio。Task)mysterymeatasyncio。ensurefuture(task)assertmysterymeatistask (L3)一个简单的什么都不做的协程函数。我们只需要一些能组成协程的东西。 (L6)我们通过直接调用该函数来创建协程对象。你的代码很少会这样做,但我想在这里明确地表示,我们正在向每个createtask()和ensurefuture()传递一个协程对象。 (L7)获取一个循环。 (L9)首先,我们使用loop。createtask()在循环中调度协程,并返回一个新的Task实例。 (L10)验证类型。到目前为止,没有什么有趣的。 (L12)我们展示了asyncio。ensurefuture()可以被用来执行与createtask()相同的动作:我们传入了一个协程,并返回了一个Task实例(并且协程已经被安排在循环中运行)!如果传入的是协程,那么loop。createtask()和asyncio。ensurefuture()之间没有区别。 (L15)如果我们给ensurefuture()传递一个Task实例会发生什么呢?注意我们要传递的Task实例是已经在第4步通过loop。createtask()创建好的。 (L16)返回的Task实例与传入的Task实例完全相同:它在被传递时没有被改变。 直接传递Future实例的意义何在?为什么用同一个函数做两件不同的事情?答案是,ensurefuture()的目的是让框架作者向最终用户开发者提供可以处理两种参数的API。不相信我?这是exBDFL自己说的: ensurefuture()的要点是,如果你有一个可能是协程或Future(后者包括一个Task,因为它是Future的子类)的东西,并且你想能够调用一个只在Future上定义的方法(可能唯一有用的例子是cancel())。当它已经是Future(或Task)时,它什么也不做;当它是协程时,它将它包装在Task中。 如果您知道您有一个协程,并且希望它被调度,那么正确的API是createtask()。唯一应该调用ensurefuture()的时候是当你提供一个API(像大多数asyncio自己的API),它接受协程或Future,你需要对它做一些事情,需要你有一个Future。 GuidovanRossum 总而言之,asyncio。surefuture()是一个为框架设计者准备的辅助函数。这一点最容易通过与一种更常见的函数进行类比来解释,所以我们来做这个解释。如果你有几年的编程经验,你可能已经见过类似于例319中的istify()函数的函数。示例319中listify()的函数。 示例319。一个强制输入列表的工具函数deflistify(x:Any)List:Tryhardtoconvertxintoalistifisinstance(x,(str,bytes)):return〔x〕try:return〔forinx〕exceptTypeError:return〔x〕 这个函数试图将参数转换为一个列表,不管输入的是什么。api和框架中经常使用这类函数将输入强制转换为已知类型,这将简化后续代码在本例中,您知道参数(来自listify()的输出)将始终是一个列表。 如果我将listify()函数重命名为ensurelist(),那么您应该开始看到与asyncio。ensurefuture()的类似之处:它总是试图将参数强制转换为Future(或子类)类型。这是一个实用函数,它使框架开发人员(而不是像你我这样的终端用户开发人员)的工作变得更容易。 实际上,asyncio标准库模块本身使用ensurefuture()正是出于这个原因。当你下次查看API时,你会发现函数参数被描述为可等待对象,很可能内部使用ensurefuture()强制转换参数。例如,asyncio。gather()函数就像下面的代码一样:asyncio。gather(aws,loopNone,。。。) aws参数表示可等待对象,包括协程、task和future。在内部,gather()使用ensurefuture()进行类型强制转换:task和future保持不变,而把协程强制转为task。 这里的关键是,作为终端用户应用程序开发人员,应该永远不需要使用asyncio。ensurefuture()。它更像是框架设计师的工具。如果你需要在事件循环上调度协程,只需直接使用asyncio。createtask()来完成。 在接下来的几节中,我们将回到语言级别的特性,从异步上下文管理器开始。