Future[T] 表示"现在还没有、稍后会到的 T";Promise[T] 是写进这个值的那一端。Future 的组合子与 Option、List 同构——第三章的 Monad 直觉在这里全额兑现。
import scala.concurrent.*, ExecutionContext.Implicits.global val f: Future[Int] = Future { expensiveCompute() // 已提交到线程池,立即返回"凭据" } f.map(_ * 2) // Future[Int] f.flatMap(n => fetchUser(n)) // Future[User],压扁两层 for n <- compute() u <- fetchUser(n) ord <- placeOrder(u) yield ord // Future[Order]
每个 Future 都需要一个 ExecutionContext(5.3 的 given 在此值班)——它决定代码在哪个线程池上跑。生产环境应显式提供专用池而非全局默认,避免把 IO 阻塞任务和 CPU 计算混进同一个池。
失败分支是类型的一部分:
fetchPrice("BTC") .recover { case ex: TimeoutException => BigDecimal(-1) } .fallbackTo(defaultPrice)
for 推导是顺序依赖的表达。两个互不依赖的请求,先后 flatMap 会白白排队:
// 串行:先等用户再查库存,总耗时相加 for u <- findUser(id); s <- findStock(id) yield (u, s) // 并行:同时出发,总耗时约等于最慢的那个 val combined = findUser(id).zip(findStock(id)) // Future[(User, Stock)] val all = Future.sequence(List(fa, fb, fc)) // Future[List[A]]
老式 API 只给你回调,没有 Future。Promise 是桥:
def fetchAsync(url: String): Future[String] = val p = Promise[String]() legacyClient.get(url, onSuccess = resp => p.success(resp.body), // 完成承诺 onFailure = ex => p.failure(ex) ) p.future // 读端照常组合
Promise 只能完成一次,第二次 complete 抛异常——语义上它就是单赋值的 val,只是赋值发生在未来。
阻塞等待仅限边界处(如测试或 main 收尾):
import scala.concurrent.duration.* Await.result(combined, 3.seconds)
在异步管线中途 Await 是典型事故来源:线程池被占满后死锁。规则是边界阻塞、内部组合。
背景:页面渲染需要用户信息、订单列表与积分三路数据,串行要 900 毫秒,并发可以压到最慢一路的耗时。操作过程:
import scala.concurrent.*, ExecutionContext.Implicits.global import scala.concurrent.duration.* def fetchUser(id: Long): Future[User] = Future { Thread.sleep(300); User(id) } def fetchOrders(u: User): Future[List[Order]] = Future { Thread.sleep(400); Nil } def fetchPoints(u: User): Future[Int] = Future { Thread.sleep(200); 65 } val page = for u <- fetchUser(1L) // 300ms ord <- fetchOrders(u) // 与积分并发:400ms pts <- fetchPoints(u) yield (u, ord, pts) page.onComplete { case Success((u, ord, pts)) => render(u, ord, pts) case Failure(e) => println(s"页面失败: ${e.getMessage}") } Await.result(page, 2.seconds) // 总耗时约 700ms 而非 900ms
结果:总耗时由三路之和变成三路之最大值。解读:for 推导在 Future 上与在 Option 上是同一套 flatMap 语义——"上一步成功才做下一步,任一步失败整体失败",第三章练的手感在这里全额兑现。变式:三路彼此不依赖时可先各自启动再 zip,还能再省 300 毫秒;这是 for 推导(串行依赖)与 zip(并行独立)的分界线,写并发前先画依赖图。
Future 描述"将有的值",Promise 是"承诺将来交付"的写端。典型场景是把旧式回调 SDK 包一层:
val p = Promise[String]() legacySdk.fetch("k", (err, data) => // 回调风格的老 SDK if err == null then p.success(data) else p.failure(err)) p.future // 拿到 Future,接入 for 推导
一个 Promise 只能完成一次,第二次 success 抛异常——这本身就是并发语义的保护。包好回调后,老 SDK 与新代码之间就只剩下 Future 一种货币。