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)
这种隔离让任务更容易推理,也带来两个实际成本:
- 主进程中的大对象可能需要复制或序列化给 worker;
- 每个 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)
doFuture 与 foreach
已有 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 调用,因为框架需要知道究竟哪些计算会使用随机数。
调试与排错顺序
并行错误容易被进程边界遮住。最稳妥的排查顺序是:
- 先用
plan(sequential)运行同一段代码; - 确认函数没有依赖未声明的交互状态、连接或外部指针;
- 再切到
multisession,从少量 workers 和少量输入开始; - 用
Sys.getpid()确认任务是否真的进入不同进程; - 比较顺序与并行结果,而不只比较运行时间;
- 完成后调用
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() 选择执行环境。