12R 并行计算:future

用 future 统一组织顺序、并行与异步任务,并在 Windows、本地多进程和批量计算中正确选择后端、处理随机数与控制内存开销。

2026-02-27
RParallel ComputingfutureAsync
本章目录 · 11

future 的价值不只是提供了几个并行函数,而是把“要计算什么”和“在哪里计算”分开。计算可以先写成 future,再通过 plan() 决定它是在当前 R 会话、后台进程,还是远程集群执行。

这让同一份分析代码可以先顺序调试,再切换到本地并行;真正需要扩展时,也不必重写任务本身。

先建立一个最小模型

future 是一个稍后才能取得结果的计算。它只有两个关键状态:

  • unresolved:计算尚未完成;
  • resolved:计算已完成,可以取得结果。

future() 创建任务,value() 取得结果。如果结果还没准备好,value() 会等待;resolved() 则只检查状态,不会为了取值而阻塞。

library(future)

plan(multisession, workers = 2)

job <- future({
  Sys.sleep(2)
  42
})

resolved(job) # 此时可能还是 FALSE
value(job)    # 必要时等待,然后返回 42

# 用完后台 workers 后显式关闭
plan(sequential)

%<-% 提供了另一种写法。它把 future 结果绑定为一个延迟求值的变量,第一次真正读取变量时才等待结果:

library(future)

plan(multisession, workers = 2)

x %<-% {
  Sys.sleep(2)
  42
}

x # 读取时等待并得到 42

plan(sequential)

需要检查状态、集中管理多个任务时,显式的 future() 对象更清楚;只是启动少量独立任务时,%<-% 更简洁。

plan() 决定任务在哪里执行

future 默认使用 sequential。并行不是加载包之后自动发生的,只有选择并行后端,并且同时存在多个可执行任务时,才可能缩短总时间。

后端 执行位置 适合场景 主要限制
sequential 当前 R 进程 开发、调试、确认结果 不并行
multisession 本机多个后台 R 会话 跨平台的本地并行 需要向 worker 传输对象
multicore 本机 fork 进程 支持 fork 的非 GUI 环境 Windows 不支持,在 RStudio 等环境也可能被禁用
cluster 本机或远程 R 会话 多台机器或已有集群 需要管理连接与运行环境

日常脚本优先从 multisession 开始,尤其是在 Windows 上:

library(future)

plan(multisession, workers = 4)

不要在可复用函数或 R 包内部擅自固定全局 plan()。执行环境应由最终运行代码的人决定;函数只负责表达任务。如果确实需要临时后端,应限制它的作用域并在退出时恢复。

进程隔离意味着什么

multisession 在独立 R 会话中执行任务。future 创建时需要的全局对象和包会被识别并传给 worker,但 worker 内部的赋值不会反向修改主会话。

library(future)

plan(multisession, workers = 2)

a <- 1
job <- future({
  a <- 2
  a * 2
})

value(job) # 4
a          # 仍然是 1

plan(sequential)

这种隔离让任务更容易推理,也带来两个实际成本:

  1. 主进程中的大对象可能需要复制或序列化给 worker;
  2. 每个 worker 都是 R 进程,会占用额外内存。

因此,并行更适合耗时明显、彼此独立的任务。若单个任务只需几毫秒,启动 worker 和传输数据的开销可能比计算本身更大。

批量任务使用上层接口

手工创建 future 适合理解模型或控制少量任务。对一批输入执行同一个函数时,通常使用与现有代码风格匹配的上层接口。

future.apply

Base R 的 lapply() 可以直接换成 future_lapply()

library(future)
library(future.apply)

plan(multisession, workers = 4)

result <- future_lapply(
  1:100,
  function(i) simulate_once(i),
  future.seed = 20260227
)

plan(sequential)

furrr

使用 purrr 风格时,以 future_map() 替换 map()

library(future)
library(furrr)

plan(multisession, workers = 4)

result <- future_map(
  1:100,
  simulate_once,
  .options = furrr_options(seed = 20260227)
)

plan(sequential)

doFutureforeach

已有 foreach 代码可以用 doFuture 接入同一套后端:

library(future)
library(doFuture)
library(foreach)

plan(multisession, workers = 4)

result <- foreach(i = 1:100, .combine = "c") %dofuture% {
  sqrt(i)
}

plan(sequential)

这三种接口解决的是同一个问题。选择依据应是现有代码使用 Base R、purrr 还是 foreach,而不是为了并行同时引入三套写法。

随机数必须显式声明

模拟、bootstrap 和置换检验经常在 worker 中生成随机数。普通的 set.seed() 不能独自保证并行任务得到统计上可靠且可重复的随机数流;应在创建任务或批量调用时显式声明 seed。

# 单个 future
job <- future(rnorm(5), seed = 20260227)

# future assignment
x %<-% rnorm(5) %seed% 20260227

# future.apply
future_lapply(1:10, function(i) rnorm(1), future.seed = 20260227)

# furrr
future_map(1:10, ~ rnorm(1), .options = furrr_options(seed = 20260227))

这里不要设置不存在的通用开关 options(future.seed = TRUE)。seed 属于具体任务或 map 调用,因为框架需要知道究竟哪些计算会使用随机数。

调试与排错顺序

并行错误容易被进程边界遮住。最稳妥的排查顺序是:

  1. 先用 plan(sequential) 运行同一段代码;
  2. 确认函数没有依赖未声明的交互状态、连接或外部指针;
  3. 再切到 multisession,从少量 workers 和少量输入开始;
  4. Sys.getpid() 确认任务是否真的进入不同进程;
  5. 比较顺序与并行结果,而不只比较运行时间;
  6. 完成后调用 plan(sequential) 关闭后台 workers。
library(future)

main_pid <- Sys.getpid()
plan(multisession, workers = 2)

worker_pid <- value(future(Sys.getpid()))
c(main = main_pid, worker = worker_pid)

plan(sequential)

如果并行版本更慢,先检查任务粒度、传给 worker 的全局对象大小,以及是否反复创建 worker。增加核心数并不保证线性加速,内存、数据传输和外部服务都可能先成为瓶颈。

什么时候值得使用

适合使用 future 的任务通常同时满足以下条件:

  • 有多个彼此独立的计算,例如模拟、bootstrap、按文件处理或按参数拟合;
  • 单个任务足够耗时,能抵消调度与数据传输成本;
  • 希望同一份代码在顺序、本地并行和集群之间切换;
  • 需要在 Windows 与 Linux 上保持一致的任务接口。

不适合直接并行化的情况包括:任务极短、所有任务争用同一文件或数据库、每个 worker 都要复制超大对象,以及算法本身存在严格的前后依赖。

最实用的工作方式是:顺序模式写对并验证结果,批量接口表达独立任务,最后才通过 plan() 选择执行环境。

参考