R非标准求值问题:获取Promise值且不触发求值/保持外部指针有效
解决外部指针(XPtr)在fork集群中因Promise求值导致的问题
这是《Parallelize function taking external pointers (XPtr)》的后续内容,我们跳过重复的C++代码,直接聚焦核心问题:当函数参数(外部指针类型)在主进程中被求值后,就无法在fork集群的子进程中正常使用。
问题重现
先看两个对比函数:
可正常运行的版本
require(parallel) test1 <- function(a) { cl <- makeForkCluster(nnodes=2) r <- parLapply(cl, 1:5, function(i) g(a,i) ) stopCluster(cl) unlist(r) }
无法运行的版本
test2 <- function(a) { cl <- makeForkCluster(nnodes=2) p <- g(a, 0) # 这一行触发了Promise参数a的求值 r <- parLapply(cl, 1:5, function(i) g(a,i) ) stopCluster(cl) unlist(r) }
test2失败的原因很明确:g(a, 0)强制触发了a的求值,而求值后的外部指针指向主进程的内存地址,fork出来的子进程无法访问这个地址,自然就失效了。
现有Workaround的局限
Ralf Stubner曾提出一个临时解决方案,通过eval(substitute(a))在不触发原Promise求值的前提下获取内容,同时保留a的Promise状态:
test3 <- function(a) { cl <- makeForkCluster(nnodes = 2) print(pryr::promise_info(a)) b <- eval(substitute(a)) p <- g(b, 0) print(pryr::promise_info(a)) r <- parLapply(cl, 1:5, function(i) g(a,i) ) stopCluster(cl) unlist(r) }
这个方法在直接调用test3时有效,但当它被其他函数嵌套调用时就会失效:
test4 <- function(b) test1(b) # 正常运行 test5 <- function(b) test3(b) # 运行失败
调试显示,eval(substitute(a))会触发上层函数test5中Promiseb的求值,进而连锁导致test3中的a也被求值,最终还是把无效的外部指针传递给了子进程。
通用解决方案
要彻底解决这个问题,我们得抓住核心本质:fork子进程无法复用主进程创建的外部指针,因为二者的内存地址空间是独立的。基于这个前提,有两种通用思路:
思路1:延迟外部指针的创建到子进程中
不要在主进程中创建任何外部指针,而是把创建指针所需的原始参数传递给子进程,让每个子进程独立创建自己的外部指针。这样就能完全避免主进程指针在子进程失效的问题:
test_fixed <- function(a) { cl <- makeForkCluster(nnodes=2) # 主进程如果需要使用g(a,0),单独创建一个仅用于主进程的指针 p_main <- g(a, 0) # 子进程中独立创建指针并处理逻辑 r <- parLapply(cl, 1:5, function(i) { ptr <- g(a, i) # 子进程自己创建外部指针 # 这里可以添加对ptr的操作 ptr }) stopCluster(cl) unlist(r) }
思路2:用惰性封装控制求值时机
如果必须在主进程保留一些逻辑,我们可以把参数封装成惰性求值的函数,确保只有在子进程环境中才触发外部指针的创建:
# 封装参数为惰性函数,延迟求值 lazy_wrap <- function(x) { function() x } test_lazy <- function(a) { cl <- makeForkCluster(nnodes=2) # 把a包装成惰性函数,避免主进程提前求值 lazy_a <- lazy_wrap(a) # 主进程如果需要使用,调用lazy_a()触发求值(这个指针仅主进程可用) p_main <- g(lazy_a(), 0) # 子进程中调用惰性函数获取值,创建自己的指针 r <- parLapply(cl, 1:5, function(i) { g(lazy_a(), i) }) stopCluster(cl) unlist(r) }
关键总结
- 永远不要在主进程创建外部指针后传递给fork子进程,二者的内存空间隔离,子进程无法访问主进程的指针地址。
- 尽量将外部指针的创建逻辑放在子进程中,传递原始参数而非已创建的指针。
- 避免在主进程中触发任何会导致外部指针求值的操作,除非这个指针仅用于主进程自身。
内容的提问来源于stack exchange,提问作者Elvis
相关产品推荐
相关产品推荐

