未来软件包的目的是提供一种非常简单且统一的方法,以使用用户可用的各种资源来评估R表达式。
在编程中,未来是一个价值的抽象,该价值可能会在以后的某个时候可用。未来的状态可以无法解决或解决。一旦解决,该值就可以立即可用。如果在未来仍未解决的情况下查询该值,则当前过程被阻止,直到未来解决。可以检查未来是否可以解决未来的情况下解决。确切解决期货的方式以及何时解决取决于使用哪种策略来评估它们。例如,可以使用顺序策略来解决未来,这意味着它在当前的R会话中得到解决。其他策略可能是通过在当前机器上并行评估表达式或同时在计算集群上同时评估表达式来解决期货。
这是一个示例,说明了期货的基础知识。首先,考虑使用普通R代码的以下代码段:
> v <- {
+ cat( " Hello world! n " )
+ 3.14
+ }
Hello world !
> v
[ 1 ] 3.14
它通过将表达式的值分配给变量v
来起作用,然后我们打印v
的值。此外,当评估v
的表达式时,我们还会打印一条消息。
这是修改了使用期货的相同代码段:
> library( future )
> v % <- % {
+ cat( " Hello world! n " )
+ 3.14
+ }
> v
Hello world !
[ 1 ] 3.14
区别在于v
构建方式;使用普通r,我们使用<-
而在期货中,我们使用%<-%
。另一个区别是,在未来解决(不是在)和查询该值时(请参阅Vignette“输出文本”)之后输出传递。
那么为什么期货有用呢?因为我们可以选择通过简单地切换设置为:
> library( future )
> plan( multisession )
> v % <- % {
+ cat( " Hello world! n " )
+ 3.14
+ }
> v
Hello world !
[ 1 ] 3.14
有了异步期货,当前/主R进程不会阻止,这意味着在期货在后台运行的单独过程中解决期货时可以进行进一步处理。换句话说,期货为R中的并行和 /或分布式处理提供了一种简单但功能强大的结构。
现在,如果您不愿意阅读有关期货的所有细节细节,但是只想尝试一下,然后跳过使用并行和非并行评估来使用Mandelbrot演示进行播放。
期货可以隐式或明确地创建。在上面的介绍性示例中,我们使用了通过v %<-% { expr }
构造创建的隐式期货。替代方案是使用f <- future({ expr })
和v <- value(f)
构造的明确期货。有了这些,我们的示例也可以写为:
> library( future )
> f <- future({
+ cat( " Hello world! n " )
+ 3.14
+ })
> v <- value( f )
Hello world !
> v
[ 1 ] 3.14
未来构造的两种风格都同样效果(*)。隐式样式与常规R代码的编写方式最相似。原则上,您要做的就是用%<-%
替换<-
将作业转变为将来的作业。另一方面,这种简单性也可能是欺骗性的,尤其是在使用异步期货时。相比之下,明确的风格使使用期货正在使用,这降低了错误的风险,并更好地将设计传达给其他人阅读您的代码。
(*)在某些情况下,如果没有(小)修改,则不能使用%<-%
。我们将在本文档末尾使用隐式期货时的“使用隐式期货”部分返回到这一点。
总而言之,为了明确期货,我们使用:
f <- future({ expr })
- 创建未来v <- value(f)
- 获取未来的价值(如果尚未解决的话,则块)对于隐式期货,我们使用:
v %<-% { expr }
- 创造未来和对其价值的承诺为了简单起见,我们将在本文档的其余部分中使用隐式样式,但是讨论的所有内容也适用于显性期货。
未来的软件包实现了以下期货类型:
姓名 | OS | 描述 |
---|---|---|
同步: | 非平行: | |
sequential | 全部 | 依次和当前的R过程 |
异步: | 平行线: | |
multisession | 全部 | 背景R会议(在当前机器上) |
multicore | 不是Windows/不是rstudio | 分叉R过程(在当前机器上) |
cluster | 全部 | 当前,本地和/或远程计算机上的外部R会话 |
未来的软件包的设计使得也可以实施对其他策略的支持。例如,Future.Callr软件包提供了未来的后端,该后端利用Callr软件包在背景R过程中评估未来 - 它们的工作方式与multisession
期货类似,但具有一些优势。继续,Future.BatchTools软件包为BatchTools软件包支持的所有类型的集群功能(“后端”)提供了期货。具体而言,还提供了通过工作调度程序评估R表达式的期货,例如Slurm,Torque/PBS,Oracle/Sun Grid Engine(SGE)和负载共享设施(LSF)。
默认情况下,热切(即时)和同步(在当前的R会话中)对未来表达式进行了同步评估。该评估策略称为“顺序”。在本节中,我们将仔细研究这些策略,并讨论它们的共同点以及它们的不同之处。
在遵循每种不同的未来策略之前,澄清未来API的目标可能会有所帮助(未来软件包定义)。在使用期货进行编程时,执行代码的未来策略实际上并不重要。这是因为我们真的不知道用户可以访问哪些计算资源,因此评估策略的选择应由用户而不是开发人员掌握。换句话说,代码不应对所使用的期货类型(例如同步或异步)做出任何假设。
未来API的设计之一是封装任何差异,以使所有类型的未来似乎都起作用。尽管表达式可以在当前的R会话中或在全球范围内进行远程r会议,但可以在本地进行评估。在不同类型的未来之间具有一致的API和行为的另一个明显优势是,它在原型型过程中有所帮助。通常,人们在构建脚本时会使用顺序评估,后来,当脚本完全开发时,可能会打开异步处理。
因此,不同策略的默认值使得评估未来表达的结果和副作用尽可能相似。更具体地说,所有未来都是正确的:
所有评估均在本地环境(即local({ expr })
)中进行,因此作业不会影响调用环境。在外部R过程中评估时,这是很自然的,但是在当前R会话中评估时也会执行。
构建未来时,确定了全局变量。对于异步评估,将全球群体导出到将评估未来表达的R过程/会话。对于带有懒惰评估的顺序期货( lazy = TRUE
),全球群体被“冷冻”(克隆到未来的本地环境)。同样,为了防止错误地导出太大的对象,内置断言所有全球群体的总大小都小于给定的阈值(可通过选项控制,请参见help("future.options")
)。如果超过阈值,则会引发信息错误。
未来的表达仅评估一次。一旦收集了值(或错误),将适用于所有后续请求。
这是一个示例,说明所有作业均已完成本地环境:
> plan( sequential )
> a <- 1
> x % <- % {
+ a <- 2
+ 2 * a
+ }
> x
[ 1 ] 4
> a
[ 1 ] 1
现在,我们准备探索不同的未来策略。
同步期货是一个接一个地解决的,最常见的是创建它们的R过程。当解决同步未来时,它会阻止主过程直到解决。
除非另有说明,否则顺序期货是默认值。他们的设计行为与定期R评估尽可能相似,同时仍履行未来的API及其行为。这是一个说明其属性的示例:
> plan( sequential )
> pid <- Sys.getpid()
> pid
[ 1 ] 1437557
> a % <- % {
+ pid <- Sys.getpid()
+ cat( " Future 'a' ... n " )
+ 3.14
+ }
> b % <- % {
+ rm( pid )
+ cat( " Future 'b' ... n " )
+ Sys.getpid()
+ }
> c % <- % {
+ cat( " Future 'c' ... n " )
+ 2 * a
+ }
Future ' a ' ...
> b
Future ' b ' ...
[ 1 ] 1437557
> c
Future ' c ' ...
[ 1 ] 6.28
> a
[ 1 ] 3.14
> pid
[ 1 ] 1437557
由于急切的顺序评估正在进行,因此在创建的那一刻,三个期货中的每一个都立即解决。还请注意,在呼叫环境中分配了当前过程的过程ID的pid
既没有被覆盖也不被删除。这是因为在当地环境中评估了期货。由于使用了同步(UNI-)处理,因此未来的b
通过主R过程(仍然在本地环境中)解决,这就是为什么b
和pid
的值相同的原因。
接下来,我们将转向异步期货,这些期货在后台解决。根据设计,这些未来是非障碍的,也就是说,在创建后,呼叫过程可用于其他任务,包括创建其他期货。只有当呼叫过程试图访问尚未解决的未来价值时,或者在所有可用的R流程都忙于服务其他未来时,才能创建另一个异步的未来,才能阻止它。
我们从多期货期货开始,因为它们得到了所有操作系统的支持。在与调用r过程同一机器上运行的背景R会话中评估了多期未来。这是我们的多项式评估的示例:
> plan( multisession )
> pid <- Sys.getpid()
> pid
[ 1 ] 1437557
> a % <- % {
+ pid <- Sys.getpid()
+ cat( " Future 'a' ... n " )
+ 3.14
+ }
> b % <- % {
+ rm( pid )
+ cat( " Future 'b' ... n " )
+ Sys.getpid()
+ }
> c % <- % {
+ cat( " Future 'c' ... n " )
+ 2 * a
+ }
Future ' a ' ...
> b
Future ' b ' ...
[ 1 ] 1437616
> c
Future ' c ' ...
[ 1 ] 6.28
> a
[ 1 ] 3.14
> pid
[ 1 ] 1437557
我们观察到的第一件事是a
, c
和pid
的值与以前相同。但是,我们注意到b
与以前不同。这是因为未来b
在不同的R过程中评估,因此它返回另一个过程ID。
当使用多期评估时,该软件包将在背景中启动一组R会议,这些会议将通过评估其创建的表达方式来服务于多主题期货。如果所有背景会议都忙于为其他未来服务,那么下一个多期未来的创建将被封锁,直到背景会话再次可用为止。启动的背景过程总数由availableCores()
的值决定,例如
> availableCores()
mc.cores
2
该特定结果告诉我们,设置了mc.cores
选项,以便我们可以在包括主要过程在内的两个(2)个过程中使用。换句话说,有了这些设置,将有两个(2)个背景过程为多主题期货服务。 availableCores()
也对不同的选项和系统环境变量也很敏捷。例如,如果使用计算群集调度程序(例如扭矩/PBS和slurm),他们设置了特定的环境变量,指定分配给任何给定作业的内核数; availableCores()
也确认了这些。如果没有指定其他指定,则将使用机器上的所有可用核心,请参见。 parallel::detectCores()
。有关更多详细信息,请参阅help("availableCores", package = "parallelly")
。
在操作系统上,R支持分叉流程(基本上都是操作系统,除了Windows之外,在后台产卵R会话的替代方案是分配现有的R进程。在支持时使用多货期货,请指定:
plan( multicore )
就像多道期货一样,运行的最大并行过程数将由availableCores()
决定,因为在这两种情况下,评估均在本地计算机上完成。
与在后台运行的单独的R会话一起工作要快得多。原因之一是将大型全球群体导出到背景会话的开销比使用分叉时更大,因此使用共享内存。另一方面,仅读取共享内存,这意味着通过一个分叉过程(“工人”)对共享对象进行的任何修改将导致操作系统的副本。当R垃圾收集器在一个分叉的过程中运行时,这也可能发生。
另一方面,在某些R环境中,过程分叉也被认为不稳定。例如,当从rstudio进程内部运行R时,可能会导致崩溃的R会议。因此,未来的软件包在从Rstudio运行时默认情况下会禁用多矿期货。有关更多详细信息,请参见help("supportsMulticore")
。
集群期货在临时群集上评估表达式(由并行软件包实现)。例如,假设您可以访问三个节点n1
, n2
和n3
,然后可以将其用于异步评估为:
> plan( cluster , workers = c( " n1 " , " n2 " , " n3 " ))
> pid <- Sys.getpid()
> pid
[ 1 ] 1437557
> a % <- % {
+ pid <- Sys.getpid()
+ cat( " Future 'a' ... n " )
+ 3.14
+ }
> b % <- % {
+ rm( pid )
+ cat( " Future 'b' ... n " )
+ Sys.getpid()
+ }
> c % <- % {
+ cat( " Future 'c' ... n " )
+ 2 * a
+ }
Future ' a ' ...
> b
Future ' b ' ...
[ 1 ] 1437715
> c
Future ' c ' ...
[ 1 ] 6.28
> a
[ 1 ] 3.14
> pid
[ 1 ] 1437557
parallel::makeCluster()
创建的任何类型的群集都可以用于集群期货。例如,可以将上述群集明确设置为:
cl <- parallel :: makeCluster(c( " n1 " , " n2 " , " n3 " ))
plan( cluster , workers = cl )
同样,在不再需要群集时关闭群集cl
,即呼叫parallel::stopCluster(cl)
也是好的样式。但是,如果主过程终止,它将自身关闭。有关如何设置和管理此类簇的更多信息,请参见help("makeCluster", package = "parallel")
。使用plan(cluster, workers = hosts)
隐式创建的群集,其中hosts
为角色向量也将在主R会话终止时也将关闭,或者在更改未来策略时,例如通过调用plan(sequential)
。
请注意,使用自动身份验证设置(例如SSH键对),没有什么可以阻止我们使用相同的方法使用一组远程计算机。
如果要在每个节点上运行多个工人,请将节点名称与在该节点上运行的工人数量一样多次复制。例如,
> plan(cluster, workers = c(rep("n1", times = 3), "n2", rep("n3", times = 5)))
总共九名平行工人,将在n1
上经营三名工人, n2
上的一名工人,在n3
上为N3五名工人。
我们已经讨论了什么可以称为未来的“平坦拓扑”,也就是说,所有期货都是在同一环境中创建并分配到同一环境中的。但是,没有什么可以阻止我们使用未来的“嵌套拓扑”,一套期货可以在内部创建另一组期货,依此类推。
例如,以下是使用多项式评估的两个“顶级”期货( a
和b
)的示例,第二个未来( b
)依次使用两个内部期货:
> plan( multisession )
> pid <- Sys.getpid()
> a % <- % {
+ cat( " Future 'a' ... n " )
+ Sys.getpid()
+ }
> b % <- % {
+ cat( " Future 'b' ... n " )
+ b1 % <- % {
+ cat( " Future 'b1' ... n " )
+ Sys.getpid()
+ }
+ b2 % <- % {
+ cat( " Future 'b2' ... n " )
+ Sys.getpid()
+ }
+ c( b.pid = Sys.getpid(), b1.pid = b1 , b2.pid = b2 )
+ }
> pid
[ 1 ] 1437557
> a
Future ' a ' ...
[ 1 ] 1437804
> b
Future ' b ' ...
Future ' b1 ' ...
Future ' b2 ' ...
b.pid b1.pid b2.pid
1437805 1437805 1437805
通过检查流程ID,我们看到总共涉及解决期货的三个不同过程。有主要的R过程(PID 1437557), a
(PID 1437804)和b
(PID 1437805)使用了两个过程。但是,由b
嵌套的两个期货( b1
和b2
)通过与b
相同的R过程进行评估。这是因为嵌套期货使用顺序评估,除非另有说明。造成这种情况有一些原因,但是主要原因是它可以误认为我们免受大量背景过程的影响,例如通过递归电话。
为了指定不同类型的评估拓扑,除了通过多项式评估解决的第一级期货和第二级通过顺序评估解决之外,我们可以提供plan()
。首先,可以明确指定与上述相同的评估策略:
plan( list ( multisession , sequential ))
如果我们尝试进行多个多级别的多项式评估,我们实际上会得到相同的行为;
> plan( list ( multisession , multisession ))
[ ... ]
> pid
[ 1 ] 1437557
> a
Future ' a ' ...
[ 1 ] 1437901
> b
Future ' b ' ...
Future ' b1 ' ...
Future ' b2 ' ...
b.pid b1.pid b2.pid
1437902 1437902 1437902
这样做的原因也在这里保护我们免受与机器所能支持的更多流程的启动。在内部,这是通过设置mc.cores = 1
来完成的,以便像parallel::mclapply()
这样的函数会依次落后。多功能和多核心评估都是这种情况。
继续,如果我们从顺序评估开始,然后使用任何嵌套期货的多主题评估,我们就会得到:
> plan( list ( sequential , multisession ))
[ ... ]
> pid
[ 1 ] 1437557
> a
Future ' a ' ...
[ 1 ] 1437557
> b
Future ' b ' ...
Future ' b1 ' ...
Future ' b2 ' ...
b.pid b1.pid b2.pid
1437557 1438017 1438016
这清楚地表明,在调用过程(PID 1437557)中解决了a
和b
,而两个嵌套的期货( b1
和b2
)则在两个单独的R过程(PID 1438017和1438016)中解析。
话虽如此,如果我们明确指定(读取力)每个级别可用的核心数量,则确实可以使用嵌套的多项式评估策略。为了做到这一点,我们需要“调整”默认设置,这可以如下完成:
> plan( list (tweak( multisession , workers = 2 ), tweak( multisession ,
+ workers = 2 )))
[ ... ]
> pid
[ 1 ] 1437557
> a
Future ' a ' ...
[ 1 ] 1438105
> b
Future ' b ' ...
Future ' b1 ' ...
Future ' b2 ' ...
b.pid b1.pid b2.pid
1438106 1438211 1438212
首先,我们看到a
和b
在不同的过程(PID 1438105和1438106)(PID 1437557)中都解决了。其次,两个嵌套期货( b1
和b2
)在其他两个R过程(PID 1438211和1438212)中得到解决。
有关在每个级别上使用嵌套期货和不同评估策略的更多详细信息,请参见“ R:Future Topologies中的Futures”。
可以检查未来是否已经解决了未解决的情况。可以使用resolved(f)
函数来完成此操作,该函数将未来的f
作为输入。如果我们与隐式期货合作(如上所述的所有示例),我们可以使用f <- futureOf(a)
函数来从隐式中检索明显的未来。例如,
> plan( multisession )
> a % <- % {
+ cat( " Future 'a' ... " )
+ Sys.sleep( 2 )
+ cat( " done n " )
+ Sys.getpid()
+ }
> cat( " Waiting for 'a' to be resolved ... n " )
Waiting for ' a ' to be resolved ...
> f <- futureOf( a )
> count <- 1
> while ( ! resolved( f )) {
+ cat( count , " n " )
+ Sys.sleep( 0.2 )
+ count <- count + 1
+ }
1
2
3
4
5
6
7
8
9
10
> cat( " Waiting for 'a' to be resolved ... DONE n " )
Waiting for ' a ' to be resolved ... DONE
> a
Future ' a ' ... done
[ 1 ] 1438287
有时未来不是您所期望的。如果在评估未来时发生错误,则当请求未来值时,错误将传播并作为呼叫环境中的错误。例如,如果我们对产生错误的未来使用懒惰评估,我们可能会看到类似的东西
> plan( sequential )
> b <- " hello "
> a % <- % {
+ cat( " Future 'a' ... n " )
+ log( b )
+ } % lazy % TRUE
> cat( " Everything is still ok although we have created a future that will fail. n " )
Everything is still ok although we have created a future that will fail.
> a
Future ' a ' ...
Error in log( b ) : non - numeric argument to mathematical function
每次请求值时都会丢弃错误,也就是说,如果我们尝试再次获得该值将产生相同的错误(并输出):
> a
Future ' a ' ...
Error in log( b ) : non - numeric argument to mathematical function
In addition : Warning message :
restarting interrupted promise evaluation
要查看给出错误的呼叫堆栈中的最后一个呼叫,我们可以在将来使用backtrace()
函数(*),即
> backtrace( a )
[[ 1 ]]
log( a )
(*)常用的traceback()
在期货背景下未提供相关信息。此外,不幸的是,无法看到导致错误的呼叫列表(评估的表达式)。只有给出错误的呼叫(这是由于内部使用的tryCatch()
的限制)。
每当要通过懒惰评估对R表达式进行异步(并行)或顺序评估时,必须识别全局(又称“ free”)对象并将其传递给评估器。它们需要像创建未来时完全通过,因为对于懒惰的评估,全球群体可能会在创建和解决时之间发生变化。对于异步处理,需要识别全球群体的原因是使它们可以导出到评估未来的过程中。
未来的软件包试图尽可能地自动化这些任务。它在Globals软件包的帮助下进行,该软件包使用静态代码检查来识别全局变量。如果确定了全局变量,则将其捕获并提供给评估过程。此外,如果包装中定义了一个全局,则该全局不会导出。相反,可以确保在评估未来时附加相应的软件包。这不仅可以更好地反映主R会话的设置,而且还可以最大程度地减少导出全球的需求,这不仅可以节省内存,还可以节省时间和带宽,尤其是在使用远程计算节点时。
最后,应该澄清的是,仅从静态代码检查中识别全球群体是一个具有挑战性的问题。总是会有一个拐角处的情况,即自动识别全球群体失败,以便识别错误的全球群体(不关心)或一些真正的全球群体缺失(这将导致运行时错误或可能是错误的结果)。 Vignette“ R:解决方案的常见问题”提供了常见案例的示例,并解释了如何避免它们以及如何帮助包装识别全球群体或忽略错误的全球群体。 globals = c("a", "slow_sum")
这globals = list(a = 42, slow_sum = my_sum)
不够globals = list(a = 42, slow_sum = my_sum)
)。
隐式期货有一个限制,而明确的未来是不存在的。因为明确的未来就像r中的任何其他对象一样,它可以在任何地方/任何事物中分配。例如,我们可以在循环中创建其中几个并将其分配给列表,例如
> plan( multisession )
> f <- list ()
> for ( ii in 1 : 3 ) {
+ f [[ ii ]] <- future({
+ Sys.getpid()
+ })
+ }
> v <- lapply( f , FUN = value )
> str( v )
List of 3
$ : int 1438377
$ : int 1438378
$ : int 1438377
使用隐式期货时,这是不可能的。这是因为在可以使用常规<-
分配运算符的所有情况下,不能使用%<-%
分配运算符。它只能用于将未来值分配给环境(包括调用环境),就像assign(name, value, envir)
工作方式一样。但是,我们可以使用指定索引将隐式期货分配给环境,例如
> plan( multisession )
> v <- new.env()
> for ( name in c( " a " , " b " , " c " )) {
+ v [[ name ]] % <- % {
+ Sys.getpid()
+ }
+ }
> v <- as.list( v )
> str( v )
List of 3
$ a : int 1438485
$ b : int 1438486
$ c : int 1438485
在这里, as.list(v)
v
了环境中的所有未来。然后收集其值并作为常规列表返回。
如果需要数字索引,则可以使用列表环境。 listv软件包实现的列表环境是带有自定义子集操作员的常规环境,使他们可以像索引列表一样索引它们。通过使用否则将使用列表的列表环境,我们还可以使用数字索引为类似列表的对象分配隐式期货。例如,
> library( listenv )
> plan( multisession )
> v <- listenv()
> for ( ii in 1 : 3 ) {
+ v [[ ii ]] % <- % {
+ Sys.getpid()
+ }
+ }
> v <- as.list( v )
> str( v )
List of 3
$ : int 1438582
$ : int 1438583
$ : int 1438582
如前所述, as.list(v)
块,直到解决所有期货。
要查看现场插图如何评估不同类型的期货,请运行此软件包的Mandelbrot演示。首先,尝试顺序评估,
library( future )
plan( sequential )
demo( " mandelbrot " , package = " future " , ask = FALSE )
如果未使用期货,则类似于脚本如何运行。然后,尝试使用在后台运行的并行R过程来计算不同的mandelbrot平面。尝试,
plan( multisession )
demo( " mandelbrot " , package = " future " , ask = FALSE )
最后,如果您可以访问多台机器,则可以尝试设置一组工人并使用它们,例如
plan( cluster , workers = c( " n2 " , " n5 " , " n6 " , " n6 " , " n9 " ))
demo( " mandelbrot " , package = " future " , ask = FALSE )
r包装未来可在Cran上使用,可以安装在R中:
install.packages( " future " )
要安装GITUB上Git Branch develop
中可用的预释放版本,请使用:
remotes :: install_github( " futureverse/future " , ref = " develop " )
这将从源安装包裹。
要为此包裹做出贡献,请参阅progruting.md。