Promise.race 和 一种协程优化实现
在这里主要展示了通过 (Python gevent中协程的) 信号量的方式来实现更佳合理的Promise.race的竞争
当然,信号量只是一种实现,因为在个人的一个小工具里用了这种方法,于是就现成的放上来了
Promise
Promise是抽象异步处理对象以及对其进行各种操作的组件,是ES6原生提供的一个对象。
在ES6标准之前,一般通过回调的方式完成异步的操作。Promise的出现让之前嵌套的回调有序的展开。
关于Promise,相关的参考主要来源于JavaScript Promise迷你书
Promise.race
Promise.race 接受多个promise对象,并且在其中任何一个promise对象变成 FullFilled
或者 Rejected
状态的话就会开始响应后续的操作
由于promise的方法叫 race
,在接受了多个 racer
之后会让每一个 racer
都完成它们的生命周期,并不是选出冠军就结束的(可能还需要排名什么的)
在 ES6 Promises 规范中,没有取消(中断)promise对象执行的概念,我们必须要确保promise最终进入resolve or reject状态之一。也就是说Promise并不适用于 状态 可能会固定不变的处理。也有一些类库提供了对promise进行取消的操作。
1 | //example from http://liubin.org/promises-book/#ch2-promise-race |
在这里的例子可以同时看到三次log出现,两次是winner,一次是loser (10秒之后)。
所以当问起 race
的适用场景的话,现在还是有一些迷茫(讲真)。race的目的是为了尽快的获取异步过程的处理结果,才让几个promise来赛跑。按照占用资源的类型来分,如果运动员们都是消耗同类资源,无论是偏向计算型还是IO型,都会趋向于把对手挤下去,最终导致大家的处境都不是很好,况且race一定要让每一个运动员都跑完全场,那么计算型的运动员们讲使得用户的本地计算资源消耗翻倍,如果是IO型的,尤其是ajax请求,对于客户端和服务器都会造成流量的浪费和占用。
当然,race的各个运动员主要占用的也可以是不同类型的资源,一个偏重消耗CPU资源,一个偏重消耗带宽资源。然而,如果各个运动员要实现的目标都是一样的,那么一定还需要一个裁判作为仲裁机构,在冠军产生之后就要将其产出采纳,并且放弃非冠军的结果(即便这个结果可能更好,只是不够快)。在各种资源供给不确定的情况下,这样的确可以简单的获取最快的结果。然而,实际环境中,真的有多少需求是将本地的CPU消耗产物等价换成带宽消耗产物的?
其实,如果race的运动员们都占用的是同类资源,比如去请求不同服务器同一功能的API(当然,不同的请求可能是简单跨域和复杂跨域,导致请求成功的概率因为浏览器安全策略而异),只要每个请求不是动不动就要下载几M数据的那种,客户端的带宽也充裕的话,这样的race其实也不错。
为了解决资源的无谓占用,能够想到的就是在冠军产生之后就把未完成比赛的运动员都淘汰掉,撤销资源。遗憾的是 ES6
中并没有相关定义。How to cancel an EMCAScript6 (vanilla JavaScript) promise chain ,在这个问题中也提到了promise的部分三方实现,BlueBird - Promise Cancellation 。虽然这个第三方库还没有用过,猜想一下可能出现的安全问题会来自于API请求被撤销之后不确定的状态。
1 | let previewWithCounter1 = new Promise((resolve)=>{ |
上述例子中,两个请求预览并且为这个资源的预览量+1 的promise在一起race,假定 kill_the_loser
方法能够把不够快的运动员撤销掉,这个时候就需要注意死去的运动员的请求状态了。死去运动员的请求可能还没有发出,肯能已经发出,服务器可能已经接受到了预览量+1的请求,也有可能没有。所以,这里还是不要有写操作的好。
在promise.race看的差不多之后,终于到了自己有类似需求的时候了。虽然不是前端的需求,只是一个小的爬虫辞典的例子。
Python中基于信号量的Race
这个小辞典需要的效果大概是并发的请求可能的三个数据来源,并在任何一个得到有效结果之后结束所有其他来源的请求,如果都没有有效结果的话,最终返回空。
三个数据来源分别是 本地SQLite、有道词典网页请求和个人搭建的服务器的请求。和promise.race相同的是让多个promise并发执行,并获得最快的响应。不一样的是,我需要的是获得最快的有效响应,并且为了防止某一个请求长时间pending,拖延整个程序的退出时间,每一个请求都有时间限制,并且,输掉比赛,就意味着死亡。
具体和最新的代码实现放在了Github-youdao.racer 上面,使用gevent库实现。
大概的结构是一个大小和运动员数量相等的信号量 ,在这里有三个运动员,所以信号量也就是3了。
以本地SQLite请求为例:
1 | def local_sql_fetch(self): |
请求被限制在一定时间内完成,在得到非空结果之后便去拿起运动员的武器,把整个协程池都结束(也就是把运动场都砸了=。=)
1 | def racer_weapon(self, bullet, gun): |
当然,由于非冠军的运动员被砸场的时候的运行进度是一个谜,所以,大家的操作最好都是只读的。在youdao的这个小工具里,个人的私有定制化服务器并没有完全遵守这一规则,主要是因为个人服务器相对于客户端可以异步的从有道官网获取查询数据,存储到服务器的Mongodb,以备后续其他人可能的相同查询(如果这个查询结果没有提前到达客户端,那么就只能造福其他人了)。
另外,如果要忽略每一位运动员的比赛结果的话(有可能没有运动员能顺利结束比赛,都没有机会捡起砸场武器),也就是更接近 promise.race
的话,无论是 FullFilled
还是 Rejected
状态,都要将这个值作为整个比赛的结果的话,可以添加一名额外的运动员在其他运动员之后。比如当前信号量的跑道只够三名运动员比赛,那么多出来的砸场人员只能待命,直到有运动员得出比赛结果,那么这名砸场人员就可以拿到信号量,把整个协程池结束掉。好吧,依然没有想要让每个运动员都像 promise.race
一样跑完全场。
最后,如果的确需要达到 promise.race
同样的效果,也就是各个运动员并发运动,获得最快的响应,但是不会砸场的话,只需要这些协程并发,然后在每一个协程结束之前调用一个共有方法,将结果传递给它,并且记录在案,接收并且只接受一个结果就可以了。 中间可以不需要信号量的存在。
总结而言,其实要实现
- 并发执行(运动员们同时开跑)
- 最快响应(获得冠军成绩)
的话,信号量只是一种限制手段,保证协程池(或者线程池等)足够容纳所有协程(或者线程等),就能够让他们并发执行(也就是跑道足够多),要获得最快的响应的话,只需要在最快的运动员跑完之后进行唯一性的记录,忽略其他运动员的成绩就可以了。
如果只是图快,只要有协程(或者线程等)运行结束,无论成功与否,把额外的协程(或线程等)加入到协程池(或线程池)中来打扫战场就好了
一切都只是为了更快,更节约资源。
本文作者 : hellflame
原文链接 : https://hellflame.github.io/2017/07/22/promise-and-concurrent/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!