twisted.internet.defer.Deferred是Twisted异步编程的一个关键对象,理解它才能更好地使用Twisted.
Deferred类似于tornado中Future(python中已经加入Future)的概念,都可以看作是一个异步执行体的封装,对需要异步执行的代码的执行和结果通知等进行了封装,从使用上看,似乎tornado的更容易,抽象度也更高一些。
这篇文章介绍Deferred对象的特性,当函数返回Deferred对象时,你有很多种方式来使用它们。
我将会假设你对Twisted框架的基本编程模型已经比较熟悉了,比如:异步,基于回调的编程,而不是在你的程序里编写会阻塞的代码,或者通过线程的方式执行需要阻塞的代码,
需要阻塞的函数直接返回,然后当数据可用时(异步代码完成时)执行一系列的回调函数。
通过学习这篇教程,你将能够使用Twisted中更多简单的API,也将能够理解使用Twisted返回deferred对象的代码。
Deferreds
Twisted使用Deferred对象来管理回调函数序列。客户程序将一系列函数关联到deferred对象,这些函数在异步请求完成时按顺序执行(这些函数被称为回调函数,或者回调函数链),还可以关联一系列函数用于在异步请求发生错误时执行,(这些函数被称为errbacks或者errbacks chain)。
Twisted的异步库代码在异步操作返回结果时调用第一个callbacks,如果发生了错误则调用第一个errbacks,Deferred对象然后处理每个callback或者errback的结果,并将其传递给回调链上的下一个函数。
Callbacks
twisted.internet.defer.Deferred可以看作在某个时刻将会有一个返回结果的对象。
我们可以在Deferred上关联函数,一旦获取到结果这些函数将被执行。另外,Deferred允许开发者为错误注册一个函数,默认情况下对错误记录日志。
Deferred机制统一了所有的需要编写阻塞代码或者稍后执行代码的编程接口。
from twisted.internet import reactor, defer def getDummyData(inputData): """ This function is a dummy which simulates a delayed result and returns a Deferred which will fire with that result. Don't try too hard to understand this. """ print('getDummyData called') deferred = defer.Deferred() # simulate a delayed result by asking the reactor to fire the # Deferred in 2 seconds time with the result inputData * 3 reactor.callLater(2, deferred.callback, inputData * 3) return deferred def cbPrintData(result): """ Data handling function to be added as a callback: handles the data by printing the result """ print('Result received: {}'.format(result)) deferred = getDummyData(3) deferred.addCallback(cbPrintData) # manually set up the end of the process by asking the reactor to # stop itself in 4 seconds time reactor.callLater(4, reactor.stop) # start up the Twisted reactor (event loop handler) manually print('Starting the reactor') reactor.run()
Multiple callbacks
可以将多个callbacks关联到deferred对象。Deferred对象回调函数链上的第一个函数将会在结果返回时被调用,第二个回调函数接受第一个函数的结果,以此类推。为什么要这样设计,考虑twisted.enterprise.adbapi返回的deferred对象---一个SQL查询结果。一个Web模块可能会安装一个回调函数用来将查询结果转换为HTML,然后将它继续向前传递,然后twisted安装一个回调函数将其返回给HTTP客户端。如果发生了错误或者异常回调链会被绕过。
from twisted.internet import reactor, defer class Getter: def gotResults(self, x): """ The Deferred mechanism provides a mechanism to signal error conditions. In this case, odd numbers are bad. This function demonstrates a more complex way of starting the callback chain by checking for expected results and choosing whether to fire the callback or errback chain """ if self.d is None: print("Nowhere to put results") return d = self.d self.d = None if x % 2 == 0: d.callback(x*3) else: d.errback(ValueError("You used an odd number!")) def _toHTML(self, r): """ This function converts r to HTML. It is added to the callback chain by getDummyData in order to demonstrate how a callback passes its own result to the next callback """ return "Result: %s" % r def getDummyData(self, x): """ The Deferred mechanism allows for chained callbacks. In this example, the output of gotResults is first passed through _toHTML on its way to printData. Again this function is a dummy, simulating a delayed result using callLater, rather than using a real asynchronous setup. """ self.d = defer.Deferred() # simulate a delayed result by asking the reactor to schedule # gotResults in 2 seconds time reactor.callLater(2, self.gotResults, x) self.d.addCallback(self._toHTML) return self.d def cbPrintData(result): print(result) def ebPrintError(failure): import sys sys.stderr.write(str(failure)) # this series of callbacks and errbacks will print an error message g = Getter() d = g.getDummyData(3) d.addCallback(cbPrintData) d.addErrback(ebPrintError) # this series of callbacks and errbacks will print "Result: 12" g = Getter() d = g.getDummyData(4) d.addCallback(cbPrintData) d.addErrback(ebPrintError) reactor.callLater(4, reactor.stop) reactor.run()
注意:
gotResults方法中有self.d。在deferred运行或者出错之前,这个属性是None,这意味着Getter实例没有一个被执行的deferred对象。
这样做有几个好处:首先,它避免了gotResults方法将同一个deferred对象执行2次(这会导致AlreadyCalledError异常)。其次,允许2次调用getDummyData(这样会设置新的d值)。最后,它使避免了循环引用,这样python的垃圾回收就更加容易。
直观解释:
1.数据接收端需要请求数据,因此申请了一个deferred对象。
2.将callbacks关联到deferred对象。
1.当结果返回时,将调用deferred.callback(result),如果发生了错误,调用errbacks。错误通常是twisted.python.failure.Failure。
2.deferred对象触发之前添加的call/err回调,并传递结果。执行流程按照以下规则,依次执行函数链:
Errbacks
Deferred的错误处理模型模仿了python的异常处理模型。如果没有错误发生,callbacks就如上图所示的那样一个接一个的运行。
如果执行了某个errback(比如数据库查询发生了异常),会向第一个errback传递一个twisted.python.failure.Failure对象,你可以添加多个errbacks,就像callbacks那样,你可以把你的errbacks想像成python中的传统异常那样。
除非你在expect里主动抛出异常,否则异常会被捕获然后停止继续传播,然后正常流程继续执行。errbacks也是这样,除非你返回一个Failure
对象或者抛出一个异常,否则错误会停止传播,正常的callback会从这点往下继续执行(使用errback的返回值)。如果errback返回了Failure对象或者抛出了一个异常,那么会继续传递给下一个errback执行。
注意:如果errback什么也没有返回,那么默认是None,这样callback会得到None值而继续执行,这可能不是你所希望的。所以,请确认errback返回了一个Failure或者一个对于callback继续执行有意义的值。
另外,Failure对象有一个trap方法,使用它将会得到和下面代码一样的效果:
try: # code that may throw an exception cookSpamAndEggs() except (SpamException, EggException): # Handle SpamExceptions and EggExceptions ...
你可以用trap这样写:
def errorHandler(failure): failure.trap(SpamException, EggException) # Handle SpamExceptions and EggExceptions d.addCallback(cookSpamAndEggs) d.addErrback(errorHandler)
如果trap的参数没有failure中包含的异常,则异常会被重新抛出。
这里有一个容易困惑的地方,有一个twisted.internet.defer.Deferred.addCallbacks方法,与使用addCallback后再使用addErrback类似,但并不完全相同。
考虑下面的代码:
# Case 1 d = getDeferredFromSomewhere() d.addCallback(callback1) # A d.addErrback(errback1) # B d.addCallback(callback2) d.addErrback(errback2) # Case 2 d = getDeferredFromSomewhere() d.addCallbacks(callback1, errback1) # C d.addCallbacks(callback2, errback2)
如果callback1发生了错误,对于Case 1,将会使用Failure对象调用errback1; 而对于Case 2,则会调用errback2。这需要额外注意。
这2种代码的意义是:
Unhandled Errors
如果一个deferred对象在垃圾回收时还有未处理的错误(正常情况下应该被errback处理),twisted会把错误的栈写入日志文件。这意味着即使你不安装errbacks也能够获取到错误日志。这需要小心,如果你持有一个deferred对象的引用,它就不能被垃圾回收,这样你就不能获取到错误的日志(你的callback就会看起来很奇怪的不被调用,其实是产生了未处理的错误)。如果对这些不确定,你需要显式地在callback后安装一个errback,比如下面这样:
# Make sure errors get logged from twisted.python import log d.addErrback(log.err)
处理同步或者异步结果
在一些程序里,一些函数可能是同步也可能是异步的。比如,一个用户认证函数可能只是用内存中的用户名进行匹配认证,这样认证函数直接就能返回结果,也有可能需要等待网络数据(比如远程认证),这就需要返回一个deferred对象,当网络数据到达时被唤醒。然而,使用这认证函数的代码既需要处理直接返回结果,也需要处理返回deferred对象的情况。
比如,库函数authenticateUser
使用isValidUser
来对用户进行认证的代码:
def authenticateUser(isValidUser, user): if isValidUser(user): print("User is authenticated") else: print("User is not authenticated")
但是上面的代码假设isValidUser
直接返回结果,但是isValidUser
也可能返回一个deferred对象。下面就会讲述如何处理这2种情况。
下面是一个isValidUser
的同步实现:
def synchronousIsValidUser(user): ''' Return true if user is a valid user, false otherwise ''' return user in ["Alice", "Angus", "Agnes"]
下面是一个异步使用deferred的实现:
from twisted.internet import reactor, defer def asynchronousIsValidUser(user): d = defer.Deferred() reactor.callLater(2, d.callback, user in ["Alice", "Angus", "Agnes"]) return d
下面的代码就能够处理同步或者异步2种情况:
from twisted.internet import defer def printResult(result): if result: print("User is authenticated") else: print("User is not authenticated") def authenticateUser(isValidUser, user): d = defer.maybeDeferred(isValidUser, user) d.addCallback(printResult)
还有一种办法是将synchronousIsValidUser也用deferred实现,更详细地介绍请看Generating Deferreds
取消
何时要取消?
一个Deferred对象可能需要很长时间才能返回结果然后执行callback。事实上,它也可能永远不能返回结果。你的客户可能等不及。当Deferred完成时,尽管你的编写的应用代码或者库中的callback可以做任何的操作,你甚至在得到结果时也可以选择只是简单地忽略它。但是,尽管你忽略了结果,但是deferred对象可能仍在后台进行一些操作,可能会消耗一些系统资源,比如CPU,内存,网络带宽甚至磁盘空间。所以,当用户点击了取消按钮,关闭了窗口,与你的服务断开连接,或者发送了停止消息,你希望显式地指明对deferred对象的结果不再关注,这样deferred对象就可以进行清理工作并释放系统资源。
下面是一个简单的例子。你连接到一个外部服务器,但是外部服务器很慢。你希望在你的应用程序里设置一个取消按钮来结束连接尝试,这样用户就可以尝试连接到另外一个服务器。下面是这种程序的一个骨架,剩余的未实现的代码留给读者练习。
def startConnecting(someEndpoint): def connected(it): "Do something useful when connected." return someEndpoint.connect(myFactory).addCallback(connected) # ... connectionAttempt = startConnecting(endpoint) def cancelClicked(): connectionAttempt.cancel()
很明显,startConnection函数是当用户点击了连接按钮这种UI元素后被调用的,会让用户选择一个主机,然后构造一个合适的endpoint(可能会使用twisted.internet.endpoints.clientFromString
)。然后,还有一个取消按钮与cancelClicked函数关联。
当调用connectionAttempt.cancel后,将会进行如下操作:
1.如果后台正在进行连接操作,将会被终止。
2.connectionAttempt这个deferred对象将会以某种方式及时地完成。
3.就如同以CancelledError调用了deferred对象的errback一样。
你可能注意到了,这一系列的动作的顺序是非常重要的。尽管取消意味着我们希望停止后台操作,但是后台操作不一定能够立即做出响应。
即使在上面这个很简单的例子里,也有一种情况可能不能被打断:平台本地相关的域名解析操作会阻塞,因此需要在一个线程中执行;连接操作不能被取消如果正在等待一个以这种线程方式实现的域名解析。所以,你想取消的deferred对象不会立即执行callback或者errback.
一个deferred对象可能会在callack中等待另一个deferred对象。在一个callback链上的某个特定点上没有办法知道所有的deferred对象都完成了。因为在callack链上的多个层次上可能希望取消同一个deferred对象,任何层次上任何时间点上都可能会调用cancel函数。cancel函数不会抛出任何异常或者返回任何值,你可能重复调用它,甚至在一个deferred对象被执行或者没有剩余的callback时调用cancel。讲这么多的主要原因除了针对特定的例子外,还有一点就是任何初始化一个deferred对象实例的人,都有可能调用cancel函数,这个函数能够绝对地完成。理想情况下,在你发出请求后所有正在进行的服务都会停止。但是没有办法保证在多个deferred对象间的操作被取消。取消deferred对象将会尽最大努力,但是有以下几个原因:
1.Deferred对象不知道如何取消底层的操作。
2.底层操作处于不可取消的状态,比如做过一些不可逆的操作。
3.Deferred对象已经有一个结果,取消操作什么也不会做。
无论能否取消取消操作总是会成功。对于情况1,2,Deferred对象会用twisted.internet.defer.CancelledError
调用errback尽管后台操作可能还在进行。
如果被取消的deferred对象在等待另外一个deferred对象,则取消操作直接发送到另外一个deferred。
默认的取消行为
考虑下面deferred对象忽略取消的代码:
operation = Deferred() def x(result): print("Hooray, a result:" + repr(x)) operation.addCallback(x) # ... def operationDone(): operation.callback("completed")
上面的代码接受用户的请求,如果接受到取消请求可能会在operation上调用取消,这里我们使用默认的取消处理,调用取消后会发生下面情况之一:
1.如果已经调用了operationDone即deferred对象已经开始,并且已经完成。那么将不会发生任何事,operation将会得到结果而不会增加callbacks。
2.如果还没有调用operationDone即deferred对象还未开始,operation将会以CancelledError进行errback。
如果已经调用了取消,也无法通知不继续调用opeartionDone,如果稍后调用了operation.callback,通常情况下,在一个已经调用过callback的deferred对象上调用callback会发生AlreadyCalledError,这会导致一个难看的无法捕捉的堆栈。所以callback只能被调用一次,在已经调用取消的deferred对象上调用callback,你会得到一个AlreadyCalledError异常。
创建一个带有取消函数的deferred对象
假设你实现一个HTTP客户端,返回一个deferred对象等待服务端返回。取消操作最好关闭连接。为了让取消操作时做到这个,你需要在创建deferred对象时传递一个函数。
class HTTPClient(Protocol): def request(self, method, path): self.resultDeferred = Deferred( lambda ignore: self.transport.abortConnection()) request = b"%s %s HTTP/1.0\r\n\r\n" % (method, path) self.transport.write(request) return self.resultDeferred def dataReceived(self, data): # ... parse HTTP response ... # ... eventually call self.resultDeferred.callback() ...现在,如果在deferred对象上调用了cancel,HTTP请求将会被取消。需要注意,已经调用了取消后不能再调用callback.
超时
超时是取消的一种特殊情况。当我们有一个deferred任务将会处理很长时间时,我们想设置一个处理时间的上限,因此我们希望在X秒后超时。
一个简便的API是Deferred.addTimeout.默认情况下,函数会产生一个TimeoutError,如果defered对象还没开始。
import random from twisted.internet import task def f(): return "Hopefully this will be called in 3 seconds or less" def main(reactor): delay = random.uniform(1, 5) def called(result): print("{0} seconds later:".format(delay), result) d = task.deferLater(reactor, delay, f) d.addTimeout(3, reactor).addBoth(called) return d # f() will be timed out if the random delay is greater than 3 seconds task.react(main)
Deferred.addTimeout在后台调用cancel函数,但是能够区分用户调用cancel还是因为timeout取消。默认情况下,addTimeout将CancelledError转换为TimeoutError。
然而,如果你在创建deferred时指定了一个取消函数,这样取消操作可能不会产生一个CancelledError。这样,addTimeout的默认行为将会阻止callback,errback和你指定取消函数的产生的值。这有可能会很有用,比如取消或者超时应该产生一个默认值而不是错误。
addTimeout有一个可选参数onTimeoutCancel,超时后会直接调用它。如果在超时之前在deferred上调用了cancel操作则onTimeoutCancel不会被调用。
超时函数的参数是deferred对象的值(可能是一个CancelledError失败),和超时的时间。这对于一些情况很有用,可以在超时函数中记录超时时间等日志。
from twisted.internet import task, defer def logTimeout(result, timeout): print("Got {0!r} but actually timed out after {1} seconds".format( result, timeout)) return result + " (timed out)" def main(reactor): # generate a deferred with a custom canceller function, and never # never callback or errback it to guarantee it gets timed out d = defer.Deferred(lambda c: c.callback("Everything's ok!")) d.addTimeout(2, reactor, onTimeoutCancel=logTimeout) d.addBoth(print) return d task.react(main)
注意,调用deferred.addTimeout的时机决定了多少callbacks应该被超时控制。包括所有在调用addTimeout之前添加的callbacks和errbacks,不包括在这之后添加的callbacks和errbacks。Deferred对象开始后超时时间即开始计算。
DeferredList
有时候你需要在多个事件完成之后得到通知,而不是一个一个事件单独等待。比如,你可能需要等待一个列表中的所有连接都关闭。这种情况下你需要使用twisted.internet.defer.DeferredList。
要创建一个DeferredList,你只需要在创建时传递你需要等待的deferred列表:
# Creates a DeferredList dl = defer.DeferredList([deferred1, deferred2, deferred3])
现在你就可以把DeferredList看成一个deferred对象。你可以调用addCallbacks。DeferredList将会在所有deferred完成后调用callbacks.将会把所有deferred的结果列表传递给callbacks。下面是一个例子:
# A callback that unpacks and prints the results of a DeferredList def printResult(result): for (success, value) in result: if success: print('Success:', value) else: print('Failure:', value.getErrorMessage()) # Create three deferreds. deferred1 = defer.Deferred() deferred2 = defer.Deferred() deferred3 = defer.Deferred() # Pack them into a DeferredList dl = defer.DeferredList([deferred1, deferred2, deferred3], consumeErrors=True) # Add our callback dl.addCallback(printResult) # Fire our three deferreds with various values. deferred1.callback('one') deferred2.errback(Exception('bang!')) deferred3.callback('three') # At this point, dl will fire its callback, printing: # Success: one # Failure: bang! # Success: three # (note that defer.SUCCESS == True, and defer.FAILURE == False)
一个标准的DeferredList永远不会调用errbacks.但是DeferredList中deferred的失败将会调用它的errback,除非consumeErros设置为True。下面更详细地介绍了其它用来更改DeferredList默认行为的标志。
注意:
如果你想为DeferredList中的deferred单独添加callbacks,你需要注意添加的时机。向DeferredList中添加一个deferred对象会给这个deferred对象添加一个callback(这个callback运行时会检查 deferredList是否已经完成)。一个重要的事件是:这个callback会记录返回值并做为结果列表的一部分返回给deferredList的callback。
所以,如果你在向deferredList添加deferred对象之后添加callback,这个callback的返回值将不会向deferredList的callback返回。为了避免造成困惑,我们建议不要在deferred对象已经添加到deferredList之后再向它添加callback。下面是一个例子:
def printResult(result): print(result) def addTen(result): return result + " ten" # Deferred gets callback before DeferredList is created deferred1 = defer.Deferred() deferred2 = defer.Deferred() deferred1.addCallback(addTen) dl = defer.DeferredList([deferred1, deferred2]) dl.addCallback(printResult) deferred1.callback("one") # fires addTen, checks DeferredList, stores "one ten" deferred2.callback("two") # At this point, dl will fire its callback, printing: # [(1, 'one ten'), (1, 'two')] # Deferred gets callback after DeferredList is created deferred1 = defer.Deferred() deferred2 = defer.Deferred() dl = defer.DeferredList([deferred1, deferred2]) deferred1.addCallback(addTen) # will run *after* DeferredList gets its value dl.addCallback(printResult) deferred1.callback("one") # checks DeferredList, stores "one", fires addTen deferred2.callback("two") # At this point, dl will fire its callback, printing: # [(1, 'one), (1, 'two')]
其它行为
DeferredList接受三个关键字参数来改变它的行为:fireOnOneCallback,fireOnOneErrback和consumeErrors。
如果设置了fireOnOneCallback,如果deferredList中的任何一个deferred调用了callback,则它将会直接调用它的callback。fireOnOneErrback也类似。
注意,deferredList和普通的deferred对象一样,只要它调用了callback或者errback,它将不会再有进一步的操作。它会忽略任何其它的deferred对象。
firstOnOneErrback选项主要用于:你希望等待所有成功之后的结果,但是如果有一个失败了便直接得到通知。
consumeErrors用来停止deferredList在它包含的所有deferred对象的callback链中传播错误。通常创建的deferredList对它包含的deferred对象的callback,errback处理方式没有影响。停止传播错误选项,可以避免"Unhandled error in Deferred"告警,如果一个deferred对象没有添加额外的errback则会产生这种告警。
设置consumeErrors为True对其它2个参数的作用没有影响。
收集结果
DeferredList的一个常见用途是收集并行异步操作的结果。如果所有的操作都成功则成功结果,如果有一个失败则失败。在这种情况下,你可以使用twisted.internet.defer.gatherResults
from twisted.internet import defer d1 = defer.Deferred() d2 = defer.Deferred() d = defer.gatherResults([d1, d2], consumeErrors=True) def cbPrintResult(result): print(result) d.addCallback(cbPrintResult) d1.callback("one") # nothing is printed yet; d is still awaiting completion of d2 d2.callback("two") # printResult prints ["one", "two"]
consumeErros参数和DeferredList中的参数含义一样。如果设置为True,则gatherResult将会处理所有deferred对象中的可能错误。通常总是这样设置除非你每个deferred对象都添加了进一步处理的callback和errback,或者你知道它们不会失败。否则,一个失败会导致"unhandled error",这将会被Twisted记录日志。这个参数是在Twisted11.1.0中加入的。
类概览
下面是对函数返回的Deferred对象的一个API参考。这里不会列举所有的API,只是作为一个指引。
还有一个关于创建Deferred对象的概览:Generating Deferreds.
基本的callback函数:
addCallbacks(self,callback[,errback,callbackArgs,callbackKeywords,errbackArgs, errbackKeywords])
这是一个你用来与deferred交互的方法.用来向deferred对象添加一个函数对(callback,errback)参考上面的流程图。.添加的callback函数签名是myMethod(result,*methodAsrgs,**methodKeywords)
.如果你的callback添加进了callbacks, 那么, 所有 在callbackArgs
元组中的参数将会以*methodArgs的形式传递给你的callback
.
还有很多简便的函数用来addCallbacks.这里不会一一详细说明,但是为了写出简洁的代码你需要了解它们.
addCallback(callback,*callbackArgs,**callbackKeywords)
向处理链的下一个位置添加一个 callback
注意, addCallbacks需要callback的参数以元组的方式传递, addCallback将所有 的参数展开挨个传递.原因很明显: addCallbacks 不知道参数是传递给callback还是errback的,所以它们必须以元组方式传递.addCallback 知道所有的参数都是传递给 callback的, 所以可以使用Python’s “*” 和“**” 语法来收集剩余的参数。
addErrback(errback,*errbackArgs,**errbackKeywords)
在errback链中的下一个位置添加一个errback
addBoth(callbackOrErrback,*callbackOrErrbackArgs,**callbackOrErrbackKeywords)
这个函数在进程链的两边(callback,errback)都添加一个同样的函数.注意,如果你使用这个函数,那么你的callback的第一个参数的类型是不确定的!
链接Deferreds
如果你需要在一个deferred中等待另外一个,你需要做的是给deferred对象添加一个callback并在其中返回一个你要等待的deferred.
比如,如果你在deferred A的一个callback中返回了一个deferred B.那么A的进程链将会停止直到B调用了callback;这时,A中的下一个callback将会以B最后一个callback的结果作为参数继续进行。
注意:
如果一个deferred对象从自己的callback中返回了,那么结果将是不确定的。Deferred的相关代码会尝试检测这种情况并产生一个告警。在将来的版本中,将会返回一个异常。
如果这看起来很困惑,不要担心。当你遇到需要使用Deferred链的情况时,你自然会明白。如果你想自动地链接deferred对象,有一个简便的方法:
chainDeferred(otherDeferred)
将 otherDeferred
添加到 Deferred对象链的末尾.当调用了self.callback , 这个位置上进程链的callback的结果将会传递给otherDeferred.callback
.后面的进程链将不会继续影响otherDeferred。
这个函数的作用和 self.addCallbacks(otherDeferred.callback,otherDeferred.errback)一样
.
See Also
Generating Deferreds, 关于介绍如何写异步函数来返回Deferred对象。
脚注:
除非添加一个后续的callback来产生一个全新的错误-----------但是前面已经提醒过,向一个已经在DeferredList中使用的Deferred对象添加callback会引起困惑,因而要避免这样做。