考虑一种调用风格.
Promise.costly(()->receive())
.transform((packet)-> mmap.transferFrom(packet))
.sidekick(()->logging.info("saved"))
.catching((exception)->logging.error("save fail",exception))
.addListener((channel)->channel.close())
这里大致是对CompletableFutre和guava对ListeningFuture的一些包装.
稍微链式了一下,以及改动了点语义.
大致涵盖的几个data flow是.
一个正常情况下的receive packet -> save -> close.
一个sidekick是在正常receive之后的并行log部分.
一个是receive exception情况下的catching部分.
以及不管是正常还是异常情况下都并行触发的addListener部分.
最终返回的reference的value是跟transform返回的签名一致.
也就是除了transform,其他都是旁路性质.
这点跟completable是不太一样的.
虽然底层实际用的就是completable future.
这么做的原因主要是不想总是try catch.
或者像go一样检查返回值.
而且为了简单,lambda的声明都是带throw的对应的functional的版本.
所以实现上是把最初的exception一路propagate过来的.
这点跟completable/listening都算一致的.
问题主要是这个compute graph的trigger或者说traversal方式.
或者说transform这些callback的调用应该是同步还是异步的问题.
因为底层实际上就是Comp了table future.
所以实际上就是thenApply之类的调用的.
如果同步的话,实现上是会当场back trace的.
也就是当一个这个graph当中的一个stage完结之后会尝试看依赖项目是否也是可以执行.
这里的一个好处就是execution path可能尽可能地总体时间短.
因为基本上算是instant的.
这样的话,几率上来说对cache可能也会有点好处.
但这样的话为什么不直接写成正常的平坦的workflow呢.
而且可以没有那些基本的代价开销.
一个理由可能就是整体流程比较长.
或者存在一些不太确定的blcoking的部分.
所以通过这种人工的方式拆分出time slice.
某种程度上来说跟go的goroutine/chan/syscall调用时候隐切换是类似思路.
通过一种人为的软调度去提高throughput.
但是同时带来的一个问题就是可能latency相对会高一些.
因为是每个execution context的slice都会被切分并在不同的实际被queue或者stack.
所以,即使是说用async的方式处理stage.
但同样地还有是用first in first out还是last in first out的形式.
像Stream API的parallel使用的common pool就是forkjoin pool的last in first out.
这个在某种程度上来说是兼顾througput和latency的一个选择.
毕竟stack的距离可能没有queue的距离来得远,每个切换的数据访问的差异可能相对没那么大.
至少意图上来说,是有一定的cache friendly的.
但是这里同样会有个问题.
比如
LongStream.range(0,regions).paralle()
.mapToObject((region)->file.map(region))
.flatmap((region)->region.pages())
.map((page)->crc.update(page))
.collect()
这类flatmap的stream generation.
可能隐式地来说,会有一个形式上的synchronize point.
因为pipeline的某个节点是generator,而generate的动作可能又是有一定开销的.
比如这里的mmap,基本上手受限于磁盘的.
这样的话forkjoin thread的local queue/stack就应该都是这些相对比较耗时的task.
也就是说,对于其他task来说,除了enqueue/dequeue和queue work load的影响之外.
还收到来自未来的运行时的不预期的一些开销因素.
所以相对来说,LIFO的不确定性比较高.
另外一点就是对于原生Future这类从设计一开始就没有考虑callback的wrap方式.
guava的actor暴露的是一个ListenInPool的选择.
也就是扔到一个你提供的线程池里blocking get,然后再回调.
在实现的时候开始也采用了类似的做法.
类似Promise.costly就是在一个cache thread pool.
而对应的Promise.light才是在一个forkjoin pool里.
一个不太好的情况就是类似之前go的syscall造成的大量thread的问题.
把future listen在一个cache thread pool的问题就是可能某个瞬间产生大量future的话,会意外地尝试非常多的thread.
当然,一个思路就是限制thread数量.
但是引入cache pool的原因就是想避免一些意外bug或者intended的不会返回的future耗尽thread的问题.
折中的办法是先把future queue起来.
然后类似早期go的net poller,定期的poll遍历touch一下.
代价就是有额外的这个poll的delay.
另外一个模仿点就是timeout.
基本上就是select timeout channel的future形式.
scheudle一个delay的future cancle.
可能本质上来说,就是在各种模仿.
毕竟人类.
没有评论:
发表评论