fpinscala 第 7 章练习 3 深度解析:为 `Par.map2` 实现尊重超时的 Future 版本
示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载导读本文围绕《Functional Programming in Scala》配套仓库 fpinscala 中answerkey/parallelism/03.hint.md给出的练习提示深入讲解如何为并行抽象Par的map2实现一个真正尊重超时timeout的版本。你将理解朴素实现为何会失效、提示中记录耗时并扣减剩余预算这一核心思想如何落地为可运行代码并结合仓库源码exercises/parallelism/Par.scala逐行剖析完整答案掌握Future.get(timeout, units)时间记账的工程细节。练习背景这是第 7 章并行性模块中的哪道题本仓库按书本章节组织练习、提示与答案见 README.md 中的章节对照表Chapter 7: parallelism。在answerkey/parallelism/目录下01.answer.md给出了map2的扩展方法签名并注明map2不会在独立逻辑线程中求值f控制并行的唯一入口是fork02.answer.md只有一句话Keep reading - we choose a representation forParlater in the chapter.继续读——稍后章节会确定Par的表示03.hint.md就是本次的主题提示它针对练习 3修复map2使其尊重调用方传入的超时限制。仓库中对应练习骨架位于 src/main/scala/fpinscala/exercises/parallelism/Par.scala该文件已给出基于UnitFuture的朴素map2第 19–23 行并在注释中明确写道这个实现不尊重超时需要一个新的Future实现来修复。练习者要做的正是根据提示写出map2Timeouts这样的超时感知版本。提示原文解读一句话概括了整个方案03.hint.md的全文只有一句话却是整道题的灵魂In order to respect timeouts, wed need a newFutureimplementation that records the amount of time spent evaluating one future, then subtracts that time from the available time allocated for evaluating the other future.拆解这句话可以得到实现的三条纲领不能沿用现成的UnitFuture需要一个新的Future实现来承载时间记账逻辑评估第一个 future 时要记录真实耗时而不是只记录它有没有完成把第一个 future 消耗的时间从总预算中扣除把剩余时间作为第二个 future 的get超时上限从而保证两个子计算的总等待时间不会超过调用方给出的整体预算。朴素map2为什么做不到尊重超时先看仓库练习文件里已有的实现exercises/parallelism/Par.scalaprivate case class UnitFutureA extends Future[A]: def isDone true def get(timeout: Long, units: TimeUnit) get def isCancelled false def cancel(evenIfRunning: Boolean): Boolean false extension A def map2B, C(f: (A, B) C): Par[C] es val af pa(es) val bf pb(es) UnitFuture(f(af.get, bf.get))问题一目了然UnitFuture.get(timeout, units)完全忽略 timeout 参数直接返回已经持有的常量值——它只能包装已经算完的结果外层map2用无参af.get、bf.get无限期阻塞等待两个子任务调用方即使传入get(100, MILLISECONDS)也无法对整体等待时间做任何限制朴素版本是先等a等到底再等b等到底两者耗时是串行相加的任何一方卡死都会让整体无限挂起。因此练习 3 的核心矛盾是map2返回的Future必须自己管理剩余时间预算并在内部对两个子 future 分别施加递减的超时而不是各给一个满额超时。完整答案实现解析map2Timeouts配套答案文件 answerkey/parallelism/03.answer.md 给出了完整实现该实现同样收录在答案模块 src/main/scala/fpinscala/answers/parallelism/Par.scala。代码全文如下/* This version respects timeouts. Note: this implementation will not prevent repeated evaluation if multiple threads call get in parallel. We could prevent this using synchronization, but it isnt needed for our purposes here (also, repeated evaluation of pure values wont affect results). */ extension A def map2TimeoutsB, C(f: (A, B) C): Par[C] es new Future[C]: private val futureA pa(es) private val futureB pb(es) volatile private var cache: Option[C] None def isDone cache.isDefined def get() get(Long.MaxValue, TimeUnit.NANOSECONDS) def get(timeout: Long, units: TimeUnit) val timeoutNanos TimeUnit.NANOSECONDS.convert(timeout, units) val started System.nanoTime val a futureA.get(timeoutNanos, TimeUnit.NANOSECONDS) val elapsed System.nanoTime - started val b futureB.get(timeoutNanos - elapsed, TimeUnit.NANOSECONDS) val c f(a, b) cache Some(c) c def isCancelled futureA.isCancelled || futureB.isCancelled def cancel(evenIfRunning: Boolean) futureA.cancel(evenIfRunning) || futureB.cancel(evenIfRunning)步骤 1构造期只求值不阻塞es new Future[C]表明调用map2Timeouts(pb)(f)得到Par[C]后只有在真正run时才把两个子Par提交给ExecutorService得到底层Future[A]与Future[B]futureA、futureB整个过程不阻塞、不等待结果把等待行为全部推迟到get方法里。步骤 2结果缓存与完成状态volatile private var cache: Option[C] None def isDone cache.isDefinedcache保存f(a, b)的结果isDone通过它是否为Some来判断声明为volatile保证一个线程写入后其他线程读取isDone能看到最新状态答案注释同时提醒如果多个线程并发调用get本实现不阻止重复求值。要彻底避免需要引入同步如只允许第一个线程计算结果、其余等待但对纯函数求值而言重复计算不会改变结果所以练习目的下可以接受。步骤 3时间记账是核心def get(timeout: Long, units: TimeUnit) val timeoutNanos TimeUnit.NANOSECONDS.convert(timeout, units) val started System.nanoTime val a futureA.get(timeoutNanos, TimeUnit.NANOSECONDS) val elapsed System.nanoTime - started val b futureB.get(timeoutNanos - elapsed, TimeUnit.NANOSECONDS) val c f(a, b) cache Some(c) c这正是提示记录耗时、扣减剩余预算的逐行落地统一单位TimeUnit.NANOSECONDS.convert(timeout, units)把调用方传入的任意TimeUnit秒、毫秒等统一换算成纳秒后续所有记账都在纳秒尺度上进行记录起点System.nanoTime是单调时钟monotonic clock只适合测量时间间隔不会受系统时间调整影响正适合做耗时统计用满额预算等第一个结果futureA.get(timeoutNanos, TimeUnit.NANOSECONDS)——a可以吃光整个预算计算实际消耗elapsed System.nanoTime - started精确记录等待a花了多久用剩余预算等第二个结果futureB.get(timeoutNanos - elapsed, TimeUnit.NANOSECONDS)——b只能拿到总预算减去a已消耗的剩余时间。这样无论a多快多慢a b的总等待上限始终约为调用方给出的timeoutNanos组合并缓存得到a、b后调用f写入cache再返回。步骤 4无参get的语义def get() get(Long.MaxValue, TimeUnit.NANOSECONDS)无参get代表无限期等待这里用Long.MaxValue纳秒约 292 年来近似没有超时。同时它复用带超时版本保证两条路径的时间记账逻辑一致。步骤 5取消与完成状态委托def isCancelled futureA.isCancelled || futureB.isCancelled def cancel(evenIfRunning: Boolean) futureA.cancel(evenIfRunning) || futureB.cancel(evenIfRunning)把取消操作委托给两个底层 future任一被取消即视为整体被取消cancel依次尝试取消两者只要有一个取消成功就返回true。边界情况与并发语义分析从代码结构可以推断并总结以下行为特征剩余预算为负数若a耗时超过总预算timeoutNanos - elapsed为负。底层java.util.concurrent.Future.get对负数超时通常会立即抛出TimeoutException即整体超时由第二个get表现出来符合总等待不超过预算的承诺f的求值不计入预算f(a, b)在两次get之后执行其计算时间不在超时保护范围内。若需要把f的求值也纳入超时控制可以进一步把整段逻辑放进带超时保护的求值中但这不是本练习的要求单位转换的溢出风险调用方若传入极大的timeout如Long.MaxValue毫秒TimeUnit.NANOSECONDS.convert可能溢出为负值。因此无参get才选择直接使用Long.MaxValue纳秒从源头避免转换溢出线程安全cache为volatileisDone读取安全但并发get可能重复求值答案注释已明示这是有意简化。若需要单次求值语义可引入同步或原子状态机属于工程加固而非练习要求。对比Nonblocking 版本中的map2仓库还提供了非阻塞 CPS 风格的并行实现 src/main/scala/fpinscala/exercises/parallelism/Nonblocking.scala。在那里opaque type Future[A] (A Unit) Unit opaque type Par[A] ExecutorService Future[A]map2用 Actor.scala 中的Actor[Either[A,B]]合并两个结果谁先到达就暂存到Option变量双方都到齐后再通过eval(es)异步执行回调cb(f(a, b))。这种实现天然不阻塞线程但也完全没有超时概念——03.hint.md讨论的新Future实现记录耗时方案针对的是基于java.util.concurrent.Future的阻塞版本Par.scala。理解这两者的差异有助于体会以fork为唯一并行控制原语的设计取舍。验证与运行仓库基于 Scala CLI 构建见 README.md 的 Building 一节你可以按如下方式验证# 编译全部练习与答案 scala-cli compile . # 打开 REPL 手工验证 scala-cli console .在 REPL 中可构造一个极端的实验让futureB对应的任务休眠很久然后用很小的超时调用map2Timeouts的get观察其是否在预算内抛出TimeoutException从而证明超时被尊重import fpinscala.exercises.parallelism.Par.* import java.util.concurrent.* val es Executors.newFixedThreadPool(4) val slowB: Par[Int] lazyUnit { Thread.sleep(10_000); 2 } val pa: Par[Int] unit(1) val combined pa.map2Timeouts(slowB)(_ _) // 预期100 毫秒内因 TimeoutException 失败而不是挂起 10 秒 combined.run(es).get(100, TimeUnit.MILLISECONDS)注练习文件 exercises/parallelism/Par.scala 中并未预置map2Timeouts需要读者依据提示自行实现上述代码引用的是答案模块中的现成实现也可对照 answers/parallelism/Par.scala 查看完整形态。小结03.hint.md用一句话点破了并行map2支持超时的全部要点换一个会记账的Future先花时间等第一个结果再拿剩余预算等第二个结果。配套答案map2Timeouts在源码中逐行兑现了这一思路TimeUnit统一换算纳秒、System.nanoTime记录单调耗时、timeoutNanos - elapsed完成预算扣减、volatile cache支撑完成状态。掌握这一模式你就同时理解了java.util.concurrent.Future超时语义的正确用法以及函数式并行库中把时间当作显式预算来传递的常见工程手法——它也是后续练习中理解更复杂并行组合子的基础。赞分享示例工程【免费下载链接】fpinscalaCode, exercises, answers, and hints to go along with the book Functional Programming in Scala项目地址https://gitcode.com/gh_mirrors/fp/fpinscala点击查看免费下载相关推荐fpinscala 第 3 章练习题 21 深度解析用 flatMap 实现 filterfilterViaFlatMapfpinscala 第 3 章练习题 21 深度解析用 flatMap 实现 filterfilterViaFlatMap 导读 本文围绕《Scala 函示例工程fpinscala 第 3 章练习 28 详解为 Tree 数据类型实现 map 函数fpinscala 第 3 章练习 28 详解为 Tree 数据类型实现 map 函数 本文基于 fpinscala 开源仓库 answerkey/datas示例工程fpinscala 第 13 章练习 3 详解实现 Free Monad 的通用解释器 runfpinscala 第 13 章练习 3 详解实现 Free Monad 的通用解释器 run 导读 本文围绕 fpinscala 仓库中 answerkey示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

安卓手机安装油猴(Tampermonkey)全流程指南:从浏览器选择到脚本管理

安卓手机安装油猴(Tampermonkey)全流程指南:从浏览器选择到脚本管理

想给安卓手机装油猴(Tampermonkey,也叫篡改猴),这事没有想象中复杂,但确实有不少人装到一半就翻车。手机浏览器和电脑浏览器架构不一样,网页脚本没法直接运行,你先得在手机里搭一个支持扩展的环…

2026/10/13 16:14:46 阅读更多 →
Dubbo核心概念深度解析:从服务暴露到集群容错实战指南

Dubbo核心概念深度解析:从服务暴露到集群容错实战指南

1. 为什么值得花时间啃透 Dubbo 的核心概念如果你正在维护一套服务数量超过十个的分布式系统,或者正准备把单体应用拆成微服务,那你大概率绕不开 Dubbo 这个名字。它是一款高性能的 Java RPC 框架,核心价值在于让远程调用像调用本地方法一样自…

2026/10/13 16:15:01 阅读更多 →
圆柱拟合中的矩阵求逆:Eigen与手写实现及数值稳定性

圆柱拟合中的矩阵求逆:Eigen与手写实现及数值稳定性

1. 从圆柱拟合反推:为什么矩阵求逆是躲不开的一环圆柱拟合这个需求,第一次遇到的人多半会觉得它是个几何问题,跟矩阵求逆八竿子打不着。我最初接手一个点云处理的小工具时也是这么想的——给一堆三维空间里的散点,拟合出一个最贴合…

2026/10/13 18:33:41 阅读更多 →

最新新闻

Cursor额度重置教程:设备指纹清理实现合规续杯

Cursor额度重置教程:设备指纹清理实现合规续杯

简介:本资源是面向软件开发者的 Cursor 编辑器 11 月续杯实战源码包,聚焦解决免费用户模型调用受限、账号续期繁琐等实际痛点,适用于中初级开发者快速提升 AI 编程效率。压缩包共 3 个文件,含 .inscode(Cursor 插件配置…

2026/10/13 19:38:20 阅读更多 →
用Claude Code摸清老项目技术栈:三轮提示词框架与实战记录

用Claude Code摸清老项目技术栈:三轮提示词框架与实战记录

“只管去写”系列到了第二天。今天的任务很明确:用Claude分析一个老项目的技术栈。这件事听起来远没有写一个新功能那么有成就感,但只要你接过大半年之前自己写的代码、接过离职同事留下的半成品项目、或者刚入职需要快速搞清一套业务系统的底细&#xf…

2026/10/13 19:38:20 阅读更多 →
开源协作的隐形基石:高效代码评审实践指南

开源协作的隐形基石:高效代码评审实践指南

做开源项目这几年,我越来越确信一件事:代码评审(code review)是整个协作链条里最被低估的一环。很多人觉得代码写完、测试跑通、合并进去就完事了,可真正把一个项目推过几个版本之后才发现,评审才是让代码质…

2026/10/13 19:38:20 阅读更多 →
Python pickle序列化实战:从协议版本到安全红线全解析

Python pickle序列化实战:从协议版本到安全红线全解析

如果你写过 Python,一定遇到过这类需求:程序跑完把内存里的字典、列表或者某个自定义类实例保存到磁盘,下次启动再原封不动读回来。Python 为此内置了一个序列化模块叫 pickle,标题说得很直白——Pickle 是 Python 内置的序列化模…

2026/10/13 19:38:20 阅读更多 →
AnyPS5:不越狱的PS5增强方案与边界协同架构

AnyPS5:不越狱的PS5增强方案与边界协同架构

项目标题:“AnyPS5”这个名称本身带有强烈的指向性与模糊性并存的特征——它既像一个技术代号,又像一句口号;既暗示兼容性、泛用性(“Any”),又锚定在特定硬件生态(“PS5”)。但问题…

2026/10/13 19:38:20 阅读更多 →
互联网大厂薪酬结构全解析:总包、级别、绩效与谈薪技巧

互联网大厂薪酬结构全解析:总包、级别、绩效与谈薪技巧

1. 先说结论:今年这个薪酬盘子到底什么情况每年这个节点,总有人拿着某头部互联网平台的薪酬截图或者 Offer 在各个群里问“这个数正常吗”。说实话,我在这一行摸爬滚打了十几年,带过团队、做过招聘、也背过绩效,今年这…

2026/10/13 19:37:19 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →