第九部分:第二个小插曲,Deferred

作者:[email protected]http://krondo.com/?p=1825  译者:杨晓伟(采用意译)

可以从这里从头来阅读这个系列

更多关于回调的知识

稍微停下来再思考一下回调的机制。尽管对于以Twisted方式使用Deferred写一个简单的异步程序已经非常了解了,但Deferred提供更多的是只有在比较复杂环境下才会用到的功能。因此,下面我们自己想出一些复杂的环境,以此来观察当使用回调编程时会遇到哪些问题。然后,再来看看deferred是如何解决这些问题的。

因此,我们为诗歌下载客户端添加了一个假想的功能。设想一些计算机科学家发明了一种新诗歌关联算法,

Byronification引擎。这个漂亮的算法根据一首诗歌生成一首使用Lord Byron式的同样的诗歌。另外,专家们提供了其Python的接口,即:

class IByronificationEngine(Interface):
    def byronificate(poem):
        """
        Return a new poem like the original, but in the style of Lord Byron.
        Raises GibberishError if the input is not a genuine poem.
        """

像大多数高尖端的软件一样,其实现都存在着许多bugs。这意外着除了已知的异常外,这个byronificate 方法可能会抛出一些专家当时没有预料到的异常出来。

我们还可以假设这个引擎能够非常快的动作以至于我们可以在主线程中调用到而无需考虑使用reactor。下面是我们想让程序实现的效果:

1.尝试下载诗歌

2.如果下载失败,告诉用户没有得到诗歌

3.如果下载到诗歌,则转交给Byronificate处理引擎一份

4.如果引擎抛出GibberishError,告诉用户没有得到诗歌

5.如果引擎抛出其它异常,则将原始式样的诗歌立给用户

6.如果我们得到这首诗歌,则打印它

7.结束程序

这里设计是当遇到GibberishError异常则表示没有得到诗歌,因此我们直接告诉用户下载失败即可。这也许对调试没什么用处,但我们的用户关心的只是我们下载到诗歌没有。另一方面,如果引擎因为一些其它的原因而出现处理失败,那么我们将原始诗歌交给用户。毕竟,有诗歌呈现总比没有好,虽然不是用户想要的Byron样式。

下面是同步模式的代码:

try:
    poem = get_poetry(host, port) # synchronous get_poetry
except:
    print >>sys.stderr, ‘The poem download failed.‘
else:
    try:
        poem = engine.byronificate(poem)
    except GibberishError:
        print >>sys.stderr, ‘The poem download failed.‘
    except:
        print poem # handle other exceptions by using the original poem
    else:
        print poem
sys.exit()

这段代码可能经过一些重构会更加简单,但已经足以说明上面的逻辑流程。我们想升级那些最近使用deferred的客户端来使用这个功能。但这部分内容我准备把它放在第十部分。现在,我们来考虑一下,用版本3.1来实现这个功能,最后一个没有使用deferred的客户端。假设我们无需考虑处理异常,那么只是改变一下got_poem回调即可:

def got_poem(poem):
    poems.append(byron_engine.byronificate(poem))
    poem_done()

那么如果byronificate抛出GibberishError异常或其它异常会发生什么呢?看看第六部分的图11,我们可以得到:

1.这个异常会传播到工厂中的poem_finished回调,即激活got_poem的方法

2.由于poem_finished并没有捕获这个异常,因此其会传递到protocol中的poemReceive函数

3.然后来到connectionLost函数,仍然在protocol中

4.然后就来到Twisted的核心区,最后止步于reactor。

前面已经了解到,reactor会捕获异常并记录它而不是“崩溃”掉。但它却不会告诉用户我们的诗歌下载失败的消息。reactor并不知道任何诗歌或GibberishErrors的信息,它只是一段被设计成适应所有网络类型的通用代码,即便与诗歌无关的网络服务。(Dave这里想强调的是reactor只是做一些具有普遍意义的事情,不会单独去处理特定的问题,例如这里原GibberishErrors异常)

注意异常是如何顺着调用链传递到具有通用性代码区域。并且看到,在got_poem后面任何一步都没有可望以我们客户端的具体要求来处理异常的。这与同步代码中的方式恰恰相反。

图15揭示了一个同步客户端的调用栈:

图15:同步调用栈

main函数是最高层,意味着它可以触及整个程序,它为什么要存在,并且它是如何在整体上表现的。典型的,main函数可以触及到用户在命令行输入想让程序做什么的参数。并且它还有一个特殊的目的:为一个命令行式的客户端打印结果。

socket的connet函数,恰恰相反,其为最低层。它所知道的就是提供到指定地址的连接。它并不知道另一端是什么及我们为什么要进行连接。但connect有通用性,不管你因为何种服务要进行网络连接都可以使用它。

get_poetry在中间,它知道要取一些诗歌,但并不知道如果得不到诗歌会发生什么。因此,从connect抛出的异常会向上传递,从低层的具有通用性的代码区到高层的具有针对性的代码区,直到其传递到知道如何处理这个异常的代码区。

现在,我们再回来看看对3.1版的假想功能的实现。我们在图16里对调用栈进行了分析,当然只是说明了其中关键的函数:

图16 异步调用栈

现在问题非常清晰了:在回调中,低层的代码(reactor)调用高层的代码,其甚至还会调用更高层的代码。因此一旦出现了异常,它并不会立即被其附件(在调用栈中可触及)的代码捕获,当然附近的代码也不可能处理它。由于异常每向上传递一次,就越靠近低层那些更加不知如何处理该异常的代码。

一旦异常来到Twisted的核心代码区,游戏也就结束了。异常并不会被处理,只是被记录下来。因此我们在以最原始的回调方式使用回调时(不使用deferred),必须在其进入Twisted之间很好地处理各种异常,至少是我们知道的那些在我们自己设定的规则下会产生的异常。当然其也应该包括那些由我们自己的BUG产生的异常。

由于bug可能存在于我们代码中的每个角落,因此我们必须将每个回调都放入try/except中,这样一来所有的异常都才有可能被捕获。这对于我们的errback同样适用,因为errback中也可能含有bugs。

Deferred的优秀架构

最终还得由Deferred来帮我们解决这类问题。当一个deferred激活了一个callback或errback时,它就会捕获各种由回调抛出的异常。换句话说,deferred扮演了try/except模块,这样一来,只要我们使用deferred就无需自己来实现这一层了。那deferred是如何解决这个问题的?很简单,它传递异常给在其链上的下一个errback。

我们添加到deferred中的第一个errback回调来处理任何出错信息,信息是在deferred的errback函数调用时发出的。但第二个errback会处理任何由第一个errback或第一个callback抛出的异常,并一直按这种规则传递下去。

回忆下图12.我们假设第一对callback/errback是stage0,下面则是stage1,stage2。。。依次类推。

对于stage N来说,如果其callback或errback出错,那么stage N+1的errback就会被调用并收到一个Failure对象作为参数,同时stage N+1的callback就不会被调用了。

通过将回调函数产生的异常向在链中传递,deferred将异常抛向了高层代码。这也意味着调用deferred的callback与errback永远不会在调用都本身处引发异常(只要你仅激活deferred一次),因此,底层的代码可以放心的激活deferred而无需担心会引发异常。相反,高层代码通过向deferred中添加errback(使用addErrback)来捕获异常。

在同步代码中,异常会在其被捕获而停止传递,那么一个errback如何发出其捕获了异常这一信号呢?同样很简单:不再引发异常。这样一来,执行权就转移到了callback中来。因此对于stage N来说,不管是callback还是errback成功执行而没有抛出异常,那么stage N+1的callback就会被调用,同样,stage N+1的errback就不会被调用了。

我们来总结一下吧:

1.一个deferred有一个callback/errback对链,它们以添加到deferred中的顺序依次排列

2.stage 0,即第一对errback/callbac,会在deferred激活时调用,具体调用那个看激活deferred的方式,若是通过.errback激活,则调用errback;同样若是通过.callback激活则调用callback。(这里的errback/callback实际是指通过addBoth添加的函数)

3.如果stage N执行出现异常,则stage N+1的errback被调用,并且其参数即为stage N出现的异常

4.同样,如果stage N成功,即没有抛出异常,则N+1的callback被调用,其第一个参数为stage N的返回值。

图17更加直观的描述上述操作:

图17:deferred中的控制流程

绿色的线表示callback和errback成功执行没抛出异常,而红线表示出现了异常。这些线不仅说明了控制流程还说明了异常与返回值在链中流动的情况。图17显示了所有deferred能出现的可能路径,但实际只有一条路径会存在。图18显示了一条可能的路径:

图18:可能的deferred激活路线

图18中,deferred的.callback函数被调用了,因此激活了stage 0的callback。这个callback成功的执行而没有抛出异常,因此控制权传给了stage 1的callback。但这个callbac执行失败而抛出异常,因此控制权传给了stage 2的errback。errback成功的处理了异常,而没有再抛出异常,因此控制权传给了stage 3的callback,并且将errback的返回值作为第一个参数传了进来(即stage 3的callback中)。

图18中,可以看出,最后一个stage上的所有的回调出现异常时,都由下一层的errback来捕获并处理,但如果最后一个stage的callback或errback执行失败而抛出异常,怎么办呢?那么这个异常就会成为unhandled(未处理)。

在同步代码中,未处理的异常会导致解释器崩溃,在原始方式使用回调的代码中未处理异常会由reactor捕获并记录下来。那么未处理异常出现在deferred中会怎样呢?让我们来做个试验。运行twisted-deferred/defer-unhandled.py试试。下面是输出:

Finished
Unhandled error in Deferred: Traceback (most recent call last):
...
--- <exception caught here> ---
...
exceptions.Exception: oops

如下几点需要引起我们的注意:

1.最后一个print函数成功执行,意味着程序并没有因为出现未处理异常而崩溃。

2.其只是将跟踪栈打印出来,而没有宕掉解释器

3.跟踪栈的内容告诉我们deferred在何处捕获了异常

4.“’Unhandle”的字符在“Finished”之后出现。

之所以出现第4条是因为,这个消息只有在deferred被垃圾回收时才会打印出来。我们将在下面的部分看到其中的原因。

在同步代码中,我们可以使用raise来重新抛出一个异常而无需其它参数。同样,我们也可以在errback中这样做。deferred通过以下两点来判断callback/errback是否执行成功:

1.callback/errback “raise”一个异常,或

2.callbakc/errback返回一个Failure对象

因为errback的第一个参数就是一个Failure,因此一个errback可以在进行完其处理后可以再次抛出这个Failure

CallbacksErrbacks,成对出现

上面讨论内容中的一个问题必须要清楚:你添加callback与errback到一个defered的顺序会决定这个deferred的的整体运行情况。另一个必须搞清楚的是:在一个deferred中callback与errback往往是成对出现。有四个方法可以向一个deferred的回调链中添加callback/errback对:

  1. addCallbacks
  2. addCallback
  3. addErrback
  4. addBoth

很明显的是,第一个与第四个是向链中添加函数对。当然中间两个也向链中添加函数对。AddCallback向链中添加一个显式的callback函数与一个隐式的”pass-through“函数(实在想不出一个对应的词)。一个pass-through函数只是虚设的函数,只将其第一个参数返回。由于errback回调函数的第一个参数是Failure,因此一个“path-through”errback总是执行“失败”,即将异常传给下个errback回调。

deferred模拟器

这部分内容,没有译。其主要是帮助理解deferred,但你会发现,读其中的代码,根本更好的理解deferred。主要是我还没有理解,嘿嘿。所以就不知为不知吧。

总结

经过这些对回调的考虑,发现由于回调式编程改变了低层代码与高层代码的关系,因此让回调产生的异常直接抛到栈中并不件好事。Deferred通过将异常捕获然后将其顺着回调链传递来解决了这个问题。

我们同样意识到,原始数据(返回值)在链中被传递。结合这个两事实也就带来了这样一种场景:根据每个stage收到的结果的不同,deferred在callback与errback链中来回交错传递数据并执行。

我们将在第十部分使用些学到的知识来更新我们的客户端。

时间: 2024-10-11 17:25:16

第九部分:第二个小插曲,Deferred的相关文章

Hook的两个小插曲

看完了前面三篇文章后,这里我们来一个小插曲~~~~ 第一个小插曲,是前面文章一个CM精灵的分析,我们这里使用hook代码来搞定: 第二个小插曲,是现在一些游戏,都有了支付上限,例如每天只能花20块钱来购买. 好了,下面我们分开叙述~~~~ 0x1:第一个小插曲 CM精灵分析的时候,打开软件能得到的最初始的信息,是软件的使用时间有限制,为30分钟,我们可以找到其上下文来继续查看一下. 软件分析方面,懒得再次打字叙述了,看下面的连接即可: http://www.52pojie.cn/thread-2

毕业论文前的小插曲

早上起的不早,因为睡前和琼琼吵了架,就这样睡着了,以为早上醒来后,又睡了一觉,潜意识里,这种情况下我第二次醒来都会是十点左右,今天还好.因为今天还有紧急的任务要完成,做视频.昨天晚上就已经收到了关于视频修改的六点意见.今天就按照这六点来改,但是,改到下午,才算是初步完成, 不出意料,这一版还是有问题,说实话,我听到的时候,内心还是非常的不情愿,事实上,很多事情都是这样,在最终的due之前,永远都不算好.不论是做产品,还是写文章,即将到来的毕业论文,这个周期,将比这个小插曲还要痛苦,这个小插曲也算

【乱写代码坑人系列】小插曲(一)将类的所有属性添加为SqlCommand的参数

小插曲(一)将类的所有属性添加为SqlCommand的参数 在使用SqlCommand 执行存储过程时,如果存储过程需要参数,就必须将每个参数都输进去,虽然说可以使用AddWithValue 方法,但参数多时仍旧有些麻烦. 在需要将类的所有属性作为参数时,可以通过反射获取这个类所有的属性和值,并直接添加到参数中. 不过需要注意的是,必须保证类的属性名和参数名相同(不区分大小写),顺序无所谓. 1 private void SetSqlParameters<T>(SqlCommand cmd,

现学现用-我的第二个小小小私活

前言 写这篇文章的目的是记录下接私活的经历和分享自己从私活中学到的知识. 对于我准备讲的第二个私活,我想提下之前做的第一个私活. 第一个 一个大学同学,想找我一起帮他亲戚做个小网站,说是几百块钱,我带着一颗激动的心就开始做了,做完了他亲戚提的需求后,我就将网站发给他们,他们觉得还行,然后我问了是否还需要做其他的,后面就没有回应了,是的,没有回应了......之前说的几百块钱并没有兑现...就这样结束了,花了两周业余空闲时间做完了. 下面是打过码的截图: 第二个 对于第二个小小小私活,接到这个活的

欧洲出差的一点小插曲

这次项目一个"小小的"设备的开发,就涉及到了和美国.中国.日本.意大利.德国.印度.韩国.台湾八个国家和地区和工程师进行沟通.这充分地体现了现代社会生产中,各国发挥自己的特长,参与全球分工和合作,以获得双赢的特征.(也相当体现了英语作为一门"世界语",掌握良好英语沟通能力非常重要). 这不,这了推进项目的进度,解决与BSP交互中发生的各种问题,2014年7月,受客户委托,再次前往意大利硬件供应商进行了一段时间的现场工作.当然,本文的重点不是现场工作的内容,而是分享这

CentOS6.10安装redis-dump小插曲

前段时间发布过一篇关于redis-dump的安装和简单实用文章博文地址:http://blog.51cto.com/wujianwei/2105124昨天因开发同事要求要给另外的一个项目的redis的数据做定时备份,原本想的直接照着之前发布的文档直接部署,但是尝试了几次,总是不成功.于是才有了今天的博文的小插曲,希望能够帮助到遇到同样问题的网友们.接下来让我们一起回顾具体的部署过程: 一.服务器的环境: [[email protected] ~]# cat /etc/redhat-release

第七部分:小插曲,Deferred

作者:[email protected]http://krondo.com/?p=1682  译者:杨晓伟(采用意译) 你可以从这里从头开始阅读这个系列 回调函数的后序发展 在第六部分我们认识这样一个情况:回调是Twisted异步编程中的基础.除了与reactor交互外,回调可以安插在任何我们写的Twisted结构内.因此在使用Twisted或其它基于reactor的异步编程体系时,都意味需要将我们的代码组织成一系列由reactor循环可以激活的回调函数链. 即使一个简单的get_poetry函

大集训的第二个小总结

时间从8.2又到了8.9号,过去了一周,又考了5场试,又有学长来讲东西,但也有不少福利,先是分西瓜,后来又是分零食,昨天又分了香蕉,不错不错. 照例先说一下考试,这次数据还没下来,回头补吧. 第一次,16名. 第二次,6名. 第三次,1名. 第四次,25名. 第五次,20名. 好吧,前期发育猥琐,后期打的不好.最终刨去两个高一大佬12名,还是没进前十,算了,习惯这种心碎的感觉了. 同桌Q某犇说我贪得无厌,或许吧,但我觉得如果有一天我真的不去追求更高的能力恐怕我就会去退役了吧. 知识方面比起上次貌

vue中简单的小插曲

我们现在来学习一下vue中一些简单的小东西: 首先我们必须要引入vue.js文件哦! 1.有关文本框里的checkbox js代码: new Vue({ el:"#app", data:{ mag:" " } }) html代码: <div id="app"> <input type="checkbox" v-model="mag"> <h1>{{mag}}</h1