文章目录

Clojure 并发:future 与 promise

发布于 2026-04-05 14:08:35 · 浏览 14 次 · 评论 0 条

Clojure 并发:future 与 promise

在 Clojure 的并发工具箱中,futurepromise 是两个看似相似却各有千秋的工具。它们都能帮助我们跳出同步执行的束缚,让程序在等待结果的同时继续处理其他任务。然而,很多开发者对它们的适用场景和关键差异感到困惑。本文将用最直接的方式,带你搞懂这两个核心原语的用法和选择策略。


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. 关键差异对比

理解 futurepromise 的本质区别,是做出正确选择的前提。

特性 future promise
值的来源 自动从表达式计算得出 由你显式 deliver 写入
计算时机 创建时立即开始执行 写入时机完全由你控制
职责定位 "帮我算这个,算完给我" "我承诺这里会有个值,到时候来取"
典型场景 并行计算、后台任务 线程间通信、事件传递

用一句话区分future 是"异步计算的结果",promise 是"线程间的握手协议"。


4. 组合使用:发挥各自优势

在复杂系统中,futurepromise 常常组合使用,发挥各自的长处。

场景:你有一个 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 则更适合"生产者-消费者"模式,或者需要跨多个回调传递单一结果的场景。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文