未來軟件包的目的是提供一種非常簡單且統一的方法,以使用用戶可用的各種資源來評估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)
)。
隱式期貨有一個限制,而明確的未來是不存在的。因為明確的未來就像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。