Clojure 并发:future 与 promise
在 Clojure 的并发工具箱中,future 和 promise 是两个看似相似却各有千秋的工具。它们都能帮助我们跳出同步执行的束缚,让程序在等待结果的同时继续处理其他任务。然而,很多开发者对它们的适用场景和关键差异感到困惑。本文将用最直接的方式,带你搞懂这两个核心原语的用法和选择策略。
1. 理解 future:异步任务的抽象
future 是 Clojure 中最直观的并发抽象。它接受一个表达式,在后台线程中执行这个表达式,并返回一个 IFuture 对象。你可以通过 deref(或 @ 宏)来获取结果,如果结果还没算好,当前线程会阻塞等待。
基本用法如下:
;; 启动一个异步任务
(def result (future
(Thread/sleep 2000) ;; 模拟耗时操作
42))
;; 主线程继续执行,不会被阻塞
(println "不等结果,我先执行")
;; 需要结果时,获取它(这里会阻塞直到结果完成)
(println "结果:" @result)
这段代码的执行流程是:future 启动后立即返回一个 IFuture 实例,主线程可以继续做其他事情。当需要实际结果时,使用 @result 触发阻塞等待。
future 的核心特性:
- 自动提交线程池:
future使用 Clojure 内置的线程池,无需手动管理线程生命周期 - 惰性求值:表达式在
future调用时立即开始执行,而不是等到你 deref 的时候 - 结果缓存:一旦计算完成,结果会被缓存,多次 deref 不会重新计算
- 异常传播:如果后台任务抛出异常,
deref时异常会被重新抛出
;; 异常处理的例子
(def fail-future (future
(throw (Exception. "出错了"))))
;; 这里会抛出 RuntimeException,包装了原始异常
@fail-future
2. 理解 promise:数据流的契约
promise 是一种承诺机制,它代表一个"将来一定会被写入的值"。与 future 不同,promise 创建时只是一个空容器,你需要在其他地方、甚至是其他线程中显式地写入(deliver)这个值。
基本用法如下:
;; 创建一个 promise
(def p (promise))
;; 在另一个线程中交付值
(future (deliver p "宇宙的终极答案"))
;; 主线程等待并获取值
(println @p) ;; 输出: 42
promise 的典型应用场景是线程间的数据传递。一个或多个线程可以等待 promise,另一个线程负责在准备好数据后 deliver 出去。这相当于在并发组件之间建立了一条单向数据通道。
一个更实际的例子:假设你有一个耗时计算,你想让它在后台运行,同时允许在多个地方等待结果:
(defn compute-intensive-task []
(let [result-promise (promise)]
;; 启动计算
(future
(let [result (heavy-calculation)]
(deliver result-promise result)))
result-promise))
;; 使用
(def result-chan (compute-intensive-task))
;; 多个地方可以同时等待同一个 promise
(println "第一部份结果: " @result-chan)
(println "第二部份结果: " @result-chan)
promise 的核心特性:
- 手动控制:
promise只是一个占位符,什么时候写入值完全由你决定 - 一次性写入:
deliver只能调用一次,多次调用会抛出异常 - 广播语义:多个线程可以同时
deref同一个promise,所有线程都会在值写入后被唤醒 - 超时支持:
deref可以带超时参数,避免无限等待
;; 超时示例
(if-let [result (deref promise-obj 1000 :超时默认值)]
result
:超时默认值)
3. 关键差异对比
理解 future 和 promise 的本质区别,是做出正确选择的前提。
| 特性 | future |
promise |
|---|---|---|
| 值的来源 | 自动从表达式计算得出 | 由你显式 deliver 写入 |
| 计算时机 | 创建时立即开始执行 | 写入时机完全由你控制 |
| 职责定位 | "帮我算这个,算完给我" | "我承诺这里会有个值,到时候来取" |
| 典型场景 | 并行计算、后台任务 | 线程间通信、事件传递 |
用一句话区分:future 是"异步计算的结果",promise 是"线程间的握手协议"。
4. 组合使用:发挥各自优势
在复杂系统中,future 和 promise 常常组合使用,发挥各自的长处。
场景:你有一个 Web 服务,需要并行调用多个外部 API,然后把结果聚合返回。你可以先用 promise 创建一个结果容器,然后用 future 并行发起多个请求,每个请求完成后 deliver 结果到 promise。
(defn fetch-all-parallel [urls]
(let [results (repeatedly (count urls) promise)]
;; 为每个 URL 启动一个请求任务
(doseq [[url p] (map vector urls results)]
(future
(try
(let [response (http-get url)]
(deliver p {:url url :status :success :body response}))
(catch Exception e
(deliver p {:url url :status :error :error e}))))
;; 等待所有结果或超时
(mapv #(deref % 5000 {:status :timeout}) results)))
这个模式的关键在于:promise 负责结果聚合的契约,future 负责并行的执行。你可以控制整体超时(通过 deref 的第二个参数),每个请求独立进行,互不阻塞。
5. 常见陷阱与避坑指南
陷阱一:对 future 使用 deliver
这是新手常犯的错误。future 创建时表达式就开始执行,你不能也不应该手动 deliver 值给它。如果你想完全控制值的写入时机,应该使用 promise。
陷阱二:promise 忘记 deliver
如果你创建了一个 promise 但永远不去 deliver,任何等待它的线程都会无限阻塞。实际开发中,建议始终配合超时使用:
;; 安全的 promise 使用模式
(let [p (promise)]
(future (deliver p (do-some-work)))
(let [result (deref p 5000 ::timeout)]
(if (= result ::timeout)
(handle-timeout!)
result)))
陷阱三:滥用阻塞
@ 操作符会阻塞当前线程。如果你在一个 Web 请求处理器的线程池中使用 @ 等待 future 完成,而 future 执行时间很长,你会快速耗尽可用线程。正确的做法是非阻塞地检查状态,或使用 realized? 先判断是否已完成:
;; 检查 future 是否已完成
(when (realized? my-future)
(do-something @my-future))
6. 实践建议
选择 future 还是 promise,可以遵循这个简单的决策树:
需要后台执行计算,且计算逻辑确定时 → 用 future
需要在线程/组件之间传递数据,且写入时机不确定时 → 用 promise
需要协调多个异步结果,且希望有明确的超时控制时 → 组合使用
在实际项目中,future 适合绝大多数"并行计算"的场景,比如批量数据处理、并行爬虫等。而 promise 则更适合"生产者-消费者"模式,或者需要跨多个回调传递单一结果的场景。

暂无评论,快来抢沙发吧!