[英]How to use `foreach` and `%dopar%` with an `R6` class in R?
我遇到了一个试图将%dopar%
和foreach()
与R6
类一起使用的问题。 搜索周围,我只能找到两个与此相关的资源,一个未解答的SO问题和R6
存储库上的一个开放的GitHub问题 。
在一个注释(即GitHub问题)中,通过将类的parent_env
重新分配为SomeClass$parent_env <- environment()
来建议解决方法。 我想知道究竟是什么environment()
是指当这个表达式(即, SomeClass$parent_env <- environment()
是内调用%dopar%
的foreach
?
这是一个可重复性最小的示例:
Work <- R6::R6Class("Work",
public = list(
values = NULL,
initialize = function() {
self$values <- "some values"
}
)
)
现在,以下Task
类在构造函数中使用Work
类。
Task <- R6::R6Class("Task",
private = list(
..work = NULL
),
public = list(
initialize = function(time) {
private$..work <- Work$new()
Sys.sleep(time)
}
),
active = list(
work = function() {
return(private$..work)
}
)
)
在Factory
类中,创建Task
类,并在..m.thread()
实现foreach
。
Factory<- R6::R6Class("Factory",
private = list(
..warehouse = list(),
..amount = NULL,
..parallel = NULL,
..m.thread = function(object, ...) {
cluster <- parallel::makeCluster(parallel::detectCores() - 1)
doParallel::registerDoParallel(cluster)
private$..warehouse <- foreach::foreach(1:private$..amount, .export = c("Work")) %dopar% {
# What exactly does `environment()` encapsulate in this context?
object$parent_env <- environment()
object$new(...)
}
parallel::stopCluster(cluster)
},
..s.thread = function(object, ...) {
for (i in 1:private$..amount) {
private$..warehouse[[i]] <- object$new(...)
}
},
..run = function(object, ...) {
if(private$..parallel) {
private$..m.thread(object, ...)
} else {
private$..s.thread(object, ...)
}
}
),
public = list(
initialize = function(object, ..., amount = 10, parallel = FALSE) {
private$..amount = amount
private$..parallel = parallel
private$..run(object, ...)
}
),
active = list(
warehouse = function() {
return(private$..warehouse)
}
)
)
然后,它被称为:
library(foreach)
x = Factory$new(Task, time = 2, amount = 10, parallel = TRUE)
如果没有以下行object$parent_env <- environment()
,它会抛出一个错误(即,如其他两个链接中所述): Error in { : task 1 failed - "object 'Work' not found"
。
我想知道,(1)在foreach
分配parent_env
时有哪些潜在的缺陷,以及(2)为什么它首先起作用?
更新1:
foreach()
返回了environment()
,这样private$..warehouse
可以捕获这些环境 rlang::env_print()
(即,在foreach
结束执行之后就放置了browser()
语句)以下是它们的组成: Browse[1]> env_print(private$..warehouse[[1]])
# <environment: 000000001A8332F0>
# parent: <environment: global>
# bindings:
# * Work: <S3: R6ClassGenerator>
# * ...: <...>
Browse[1]> env_print(environment())
# <environment: 000000001AC0F890>
# parent: <environment: 000000001AC20AF0>
# bindings:
# * private: <env>
# * cluster: <S3: SOCKcluster>
# * ...: <...>
Browse[1]> env_print(parent.env(environment()))
# <environment: 000000001AC20AF0>
# parent: <environment: global>
# bindings:
# * private: <env>
# * self: <S3: Factory>
Browse[1]> env_print(parent.env(parent.env(environment())))
# <environment: global>
# parent: <environment: package:rlang>
# bindings:
# * Work: <S3: R6ClassGenerator>
# * .Random.seed: <int>
# * Factory: <S3: R6ClassGenerator>
# * Task: <S3: R6ClassGenerator>
免责声明:我在这里说的很多都是基于我所知道的有根据的猜测和推论,我不能保证一切都是100%正确的。
我认为可能存在许多陷阱,哪一个适用取决于你做了什么。 我认为你的第二个问题更为重要,因为如果你明白这一点,你将能够自己评估一些陷阱。
这个主题相当复杂,但你可以从阅读R的词汇范围开始 。 本质上,R具有一种环境层次结构,当执行R代码时,在父环境中寻找其值在当前环境中找不到的变量(即environment()
返回的变量)(不要与之混淆)呼叫者环境)。
根据您链接的GitHub问题, R6
生成器将“引用”保存到其父环境中,并且他们希望可以在所述父级或环境层次结构中的某个位置找到其所需类的所有内容,从该父级开始并“向上” ”。
您正在使用的变通方法之所以有效,是因为您将生成器的父环境替换为并行工作程序中当前foreach
调用中的一个(可能是不同的R进程,不一定是不同的线程),并且给定你的.export
规范可能会输出必要的值,然后R的词法范围可以从单独的线程/进程中的foreach
调用开始搜索缺失值。
对于您链接的具体示例,我发现使其工作的更简单方法(至少在我的Linux机器上)是执行以下操作:
library(doParallel)
cluster <- parallel::makeCluster(parallel::detectCores() - 1)
doParallel::registerDoParallel(cluster)
parallel::clusterExport(cluster, setdiff(ls(), "cluster"))
x = Factory$new(Task, time = 1, amount = 3)
但是将..m.thread
函数保留为:
..m.thread = function(object, amount, ...) {
private$..warehouse <- foreach::foreach(1:amount) %dopar% {
object$new(...)
}
}
(完成后手动调用stopCluster
)。
clusterExport
调用应该具有类似于*的语义:从主R进程的全局环境中获取除cluster
之外的所有内容,并使其在每个并行工作程序的全局环境中可用。 这样,当词法作用域到达各自的全局环境时, foreach
调用中的任何代码都可以使用生成器。 foreach
可以很聪明并自动导出一些变量(如GitHub问题所示),但它有局限性,并且在词法作用域中使用的层次结构会变得非常混乱。
*我说“类似于”因为我不知道如果使用叉子,R对于区分(全局)环境究竟是什么,但由于需要输出,我认为它们确实是彼此独立的。
PS:如果你在函数调用中创建worker,我会使用对on.exit(parallel::stopCluster(cluster))
调用,这样你就可以避免在发生错误时以某种方式停止进程。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.