Le but du futur est de fournir un moyen très simple et uniforme d'évaluer les expressions R à l'aide de diverses ressources disponibles pour l'utilisateur.
En programmation, un avenir est une abstraction pour une valeur qui pourrait être disponible à un moment donné dans le futur. L'état d'un avenir peut être non résolu ou résolu . Dès qu'il est résolu, la valeur est disponible instantanément. Si la valeur est interrogée alors que l'avenir n'est toujours pas résolu, le processus actuel est bloqué jusqu'à ce que l'avenir soit résolu. Il est possible de vérifier si un avenir est résolu ou non sans blocage. Exactement comment et quand les futurs sont résolus dépend de la stratégie utilisée pour les évaluer. Par exemple, un avenir peut être résolu à l'aide d'une stratégie séquentielle, ce qui signifie qu'il est résolu dans la session R actuelle. D'autres stratégies peuvent être de résoudre les futurs de manière asynchrone, par exemple, en évaluant les expressions en parallèle sur la machine actuelle ou simultanément sur un cluster de calcul.
Voici un exemple illustrant comment fonctionnent les bases de l'avenir. Tout d'abord, considérez l'extrait de code suivant qui utilise le code R PLAIR:
> v <- {
+ cat( " Hello world! n " )
+ 3.14
+ }
Hello world !
> v
[ 1 ] 3.14
Il fonctionne en attribuant la valeur d'une expression à la variable v
et nous imprimons ensuite la valeur de v
. De plus, lorsque l'expression de v
est évaluée, nous imprimons également un message.
Voici le même extrait de code modifié pour utiliser les futures à la place:
> library( future )
> v % <- % {
+ cat( " Hello world! n " )
+ 3.14
+ }
> v
Hello world !
[ 1 ] 3.14
La différence est dans la façon dont v
est construit; Avec Plain R, nous utilisons <-
alors qu'avec les avenir, nous utilisons %<-%
. L'autre différence est que la sortie est relayée après la résolution de l'avenir (pas pendant) et lorsque la valeur est interrogée (voir Vignette «Texte de sortie»).
Alors pourquoi les futurs sont-ils utiles? Parce que nous pouvons choisir d'évaluer l'expression future dans un processus R séparé de manière asynchrone en changeant simplement les paramètres comme:
> library( future )
> plan( multisession )
> v % <- % {
+ cat( " Hello world! n " )
+ 3.14
+ }
> v
Hello world !
[ 1 ] 3.14
Avec les contrats à terme asynchrones, le processus R actuel / principal ne bloque pas , ce qui signifie qu'il est disponible pour un traitement ultérieur tandis que les contrats à terme sont résolus dans des processus distincts exécutés en arrière-plan. En d'autres termes, les futurs fournissent une construction simple mais pourtant puissante pour un traitement parallèle et / ou distribué dans R.
Maintenant, si vous ne pouvez pas être dérangé de lire tous les détails de Nitty-Gritty sur les futurs, mais que vous voulez simplement les essayer, puis sautez jusqu'à la fin pour jouer avec la démo Mandelbrot en utilisant à la fois une évaluation parallèle et non parallèle.
Les futurs peuvent être créés implicitement ou explicitement . Dans l'exemple d'introduction ci-dessus, nous avons utilisé des futurs implicites créés via la construction v %<-% { expr }
. Une alternative est un futur explicite à l'aide des constructions f <- future({ expr })
et v <- value(f)
. Avec ceux-ci, notre exemple pourrait également être écrit comme:
> library( future )
> f <- future({
+ cat( " Hello world! n " )
+ 3.14
+ })
> v <- value( f )
Hello world !
> v
[ 1 ] 3.14
L'un ou l'autre style de construction future fonctionne également (*) bien. Le style implicite est le plus similaire à la façon dont le code R régulier est écrit. En principe, tout ce que vous avez à faire est de remplacer <-
par un %<-%
pour transformer la cession en une affectation future. D'un autre côté, cette simplicité peut également être trompeuse, en particulier lorsque des avenir asynchrones sont utilisés. En revanche, le style explicite rend beaucoup plus clair que les avenir sont utilisés, ce qui réduit le risque d'erreurs et communique mieux la conception à d'autres lisant votre code.
(*) Il y a des cas où %<-%
ne peut pas être utilisé sans certaines (petites) modifications. Nous reviendrons à cela dans les contraintes de la section lors de l'utilisation d'Implicit Futures 'vers la fin de ce document.
Pour résumer, pour des futurs explicites, nous utilisons:
f <- future({ expr })
- crée un avenirv <- value(f)
- obtient la valeur du futur (blocs sinon résolu)Pour les futurs implicites, nous utilisons:
v %<-% { expr }
- crée un avenir et une promesse à sa valeurPour rester simple, nous utiliserons le style implicite dans le reste de ce document, mais tout ce qui est discuté s'appliquera également aux futurs explicites.
Le futur package implémente les types de futurs suivants:
Nom | Oscilatoires | Description |
---|---|---|
synchrone: | Non-parallèle: | |
sequential | tous | séquentiellement et dans le processus r actuel |
asynchrone: | parallèle : | |
multisession | tous | Sessions de fond R (sur la machine actuelle) |
multicore | Pas Windows / pas Rstudio | Processus R Forks (sur la machine actuelle) |
cluster | tous | Sessions R externes sur les machines actuelles, locales et / ou éloignées |
Le futur package est conçu de manière à ce que le soutien à des stratégies supplémentaires puisse également être mis en œuvre. Par exemple, le package Future.Callr fournit des backends futurs qui évaluent les futurs dans un processus de fond R en utilisant le package Callr - ils fonctionnent de manière similaire aux futurs multisession
mais présente quelques avantages. Poursuivant, le package Future.BatchTools fournit un avenir pour tous les types de fonctions de cluster ("backends") que le package BatchTools prend en charge. Plus précisément, les contrats à terme sur l'évaluation des expressions R via des planificateurs de travail tels que Slurm, Torque / PBS, Oracle / Sun Grid Engine (SGE) et Facility Caware Facility (LSF) sont également disponibles.
Par défaut, les expressions futures sont évaluées avec impatience (= instantanément) et de manière synchrone (dans la session R actuelle). Cette stratégie d'évaluation est appelée "séquentielle". Dans cette section, nous passerons par chacune de ces stratégies et discuterons de ce qu'ils ont en commun et de la façon dont ils diffèrent.
Avant de passer par chacune des différentes stratégies futures, il est probablement utile de clarifier les objectifs de la future API (telle que définie par le futur package). Lors de la programmation avec des futures, cela ne devrait pas vraiment avoir d'importance quelle stratégie future est utilisée pour exécuter du code. En effet, nous ne pouvons pas vraiment savoir aux ressources informatiques auxquelles l'utilisateur a accès afin que le choix de la stratégie d'évaluation soit entre les mains de l'utilisateur et non du développeur. En d'autres termes, le code ne doit faire aucune hypothèse sur le type de contrat utilisé, par exemple synchrone ou asynchrone.
L'une des conceptions de la future API était d'encapsuler toutes les différences de telle sorte que tous les types de futurs semblent fonctionner de la même manière. Cela malgré les expressions peut être évalué localement dans la session R actuelle ou à travers le monde en sessions R éloignées. Un autre avantage évident d'avoir une API et un comportement cohérents entre différents types de futurs est qu'il aide lors du prototypage. En règle générale, on utiliserait une évaluation séquentielle lors de la construction d'un script et, plus tard, lorsque le script est complètement développé, on peut activer le traitement asynchrone.
Pour cette raison, les défaillances des différentes stratégies sont telles que les résultats et les effets secondaires de l'évaluation d'une expression future sont aussi similaires que possible. Plus précisément, ce qui suit est vrai pour tous les futurs:
Toute l'évaluation se fait dans un environnement local (c'est-à-dire local({ expr })
) afin que les affectations n'aient pas l'environnement d'appel. Ceci est naturel lors de l'évaluation dans un processus R externe, mais il est également appliqué lors de l'évaluation dans la session R actuelle.
Lorsqu'un avenir est construit, des variables globales sont identifiées . Pour l'évaluation asynchrone, les globaux sont exportés vers le processus / session qui évaluera l'expression future. Pour les futurs séquentiels avec une évaluation paresseuse ( lazy = TRUE
), les globaux sont "gelés" (clonés dans un environnement local du futur). De plus, afin de protéger contre l'exportation d'objets trop gros par erreur, il existe une affirmation intégrée que la taille totale de tous les globaux est inférieure à un seuil donné (contrôlable via une option, cf. help("future.options")
). Si le seuil est dépassé, une erreur informative est lancée.
Les expressions futures ne sont évaluées qu'une seule fois . Dès que la valeur (ou une erreur) a été collectée, elle sera disponible pour toutes les demandes suivantes.
Voici un exemple illustrant que toutes les affectations sont effectuées dans un environnement local:
> plan( sequential )
> a <- 1
> x % <- % {
+ a <- 2
+ 2 * a
+ }
> x
[ 1 ] 4
> a
[ 1 ] 1
Nous sommes maintenant prêts à explorer les différentes stratégies futures.
Les futurs synchrones sont résolus l'un après l'autre et le plus souvent par le processus R qui les crée. Lorsqu'un avenir synchrone est résolu, il bloque le processus principal jusqu'à ce qu'il soit résolu.
Les futurs séquentiels sont la valeur par défaut, sauf indication contraire. Ils ont été conçus pour se comporter aussi similaires que possible à l'évaluation R régulière tout en remplissant la future API et ses comportements. Voici un exemple illustrant leurs propriétés:
> 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
Étant donné que l'évaluation séquentielle impatient a lieu, chacun des trois contrats à terme est résolu instantanément au moment où il est créé. Notez également comment pid
dans l'environnement d'appel, qui a été attribué l'ID de processus du processus actuel, n'est ni écrasé ni supprimé. En effet, les futurs sont évalués dans un environnement local. Étant donné que le traitement synchrone (UNI-) est utilisé, le futur b
est résolu par le processus R principal (toujours dans un environnement local), c'est pourquoi la valeur de b
et pid
est la même.
Ensuite, nous nous tournerons vers les futurs asynchrones, qui sont des futurs qui sont résolus en arrière-plan. Par conception, ces futurs ne bloquent pas, c'est-à-dire après avoir été créé, le processus d'appel est disponible pour d'autres tâches, y compris la création de futurs supplémentaires. Ce n'est que lorsque le processus d'appel essaie d'accéder à la valeur d'un avenir qui n'est pas encore résolu, ou d'essayer de créer un autre avenir asynchrone lorsque tous les processus R disponibles sont occupés à servir d'autres futures, qu'il bloque.
Nous commençons par des contrats à terme sur multisession car ils sont soutenus par tous les systèmes d'exploitation. Un avenir multi-session est évalué dans une session en arrière-plan R fonctionnant sur la même machine que le processus CALLAG R. Voici notre exemple avec l'évaluation de la multisesse:
> 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
La première chose que nous observons est que les valeurs de a
, c
et pid
sont les mêmes qu'auparavant. Cependant, nous remarquons que b
est différent avant. En effet, Future b
est évaluée dans un processus R différent et il renvoie donc un ID de processus différent.
Lorsque l'évaluation de la multisesse est utilisée, le package lance un ensemble de sessions R en arrière-plan qui serviront à terme en multisesse en évaluant leurs expressions au fur et à mesure de leur création. Si toutes les séances d'arrière-plan sont occupées à servir d'autres futurs, la création du prochain avenir multi-session est bloquée jusqu'à ce qu'une session de fond redevienne disponible. Le nombre total de processus d'arrière-plan lancés est décidé par la valeur de availableCores()
, par exemple
> availableCores()
mc.cores
2
Ce résultat particulier nous indique que l'option mc.cores
a été définie de telle sorte que nous sommes autorisés à utiliser dans deux (2) processus, y compris le processus principal. En d'autres termes, avec ces paramètres, il y aura deux (2) processus d'arrière-plan au service des contrats à terme sur multisession. Le availableCores()
est également agile à différentes options et variables d'environnement système. Par exemple, si les planificateurs de cluster de calcul sont utilisés (par exemple, le couple / PBS et Slurm), ils définissent une variable d'environnement spécifique spécifiant le nombre de cœurs qui ont été alloués à un travail donné; availableCores()
les reconnaît également. Si rien d'autre n'est spécifié, tous les noyaux disponibles sur la machine seront utilisés, cf. parallel::detectCores()
. Pour plus de détails, veuillez consulter help("availableCores", package = "parallelly")
.
Sur les systèmes d'exploitation où R prend en charge la fourniture de processus, qui est essentiellement tout le système d'exploitation, sauf Windows, une alternative à la frai des séances R en arrière-plan est de déborder le processus R existant. Pour utiliser des futures multicore, lorsqu'ils sont pris en charge, spécifiez:
plan( multicore )
Tout comme pour les contrats à terme sur multisesse, le nombre maximum de processus parallèles en cours d'exécution sera décidé par availableCores()
, car dans les deux cas, l'évaluation est effectuée sur la machine locale.
La fourniture d'un processus R peut être plus rapide que de travailler avec une session R distincte en arrière-plan. L'une des raisons est que la surcharge d'exportation de grands globaux vers la session de fond peut être plus grande que lors de la fourniture, et donc la mémoire partagée, est utilisée. D'un autre côté, la mémoire partagée est uniquement lue , ce qui signifie que toutes les modifications des objets partagés par l'un des processus fourchus ("travailleurs") provoqueront une copie par le système d'exploitation. Cela peut également se produire lorsque le collecteur R Garbage fonctionne dans l'un des processus fourchus.
D'un autre côté, la fourniture de processus est également considérée comme instable dans certains environnements R. Par exemple, lors de l'exécution de R à partir de RSTUDIO, la forking peut entraîner des sessions R écrasées. Pour cette raison, le Future Package désactive les futurs multicore par défaut lorsqu'il fonctionne à partir de rstudio. Voir help("supportsMulticore")
pour plus de détails.
Les futurs en cluster évaluent les expressions sur un cluster ad hoc (comme implémenté par le package parallèle). Par exemple, supposons que vous avez accès à trois nœuds n1
, n2
et n3
, vous pouvez ensuite les utiliser pour une évaluation asynchrone comme:
> 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
Tous les types de clusters qui créent parallel::makeCluster()
peuvent être utilisés pour les futurs en grappe. Par exemple, le cluster ci-dessus peut être explicitement configuré comme:
cl <- parallel :: makeCluster(c( " n1 " , " n2 " , " n3 " ))
plan( cluster , workers = cl )
En outre, il est considéré comme un bon style pour arrêter le cluster cl
lorsqu'il n'est plus nécessaire, c'est-à-dire appeler parallel::stopCluster(cl)
. Cependant, il s'arrête si le processus principal est terminé. Pour plus d'informations sur la façon de configurer et de gérer ces clusters, consultez help("makeCluster", package = "parallel")
. Les clusters ont créé implicitement à l'aide plan(cluster, workers = hosts)
où hosts
sont un vecteur de caractères également fermé lorsque la session R principale se termine, ou lorsque la stratégie future est modifiée, par exemple en appelant plan(sequential)
.
Notez qu'avec la configuration automatique de l'authentification (par exemple, les paires de clés SSH), rien ne nous empêche d'utiliser la même approche pour utiliser un groupe de machines distantes.
Si vous souhaitez exécuter plusieurs travailleurs sur chaque nœud, reproduisez le nom du nœud autant de fois que le nombre de travailleurs à exécuter sur ce nœud. Par exemple,
> plan(cluster, workers = c(rep("n1", times = 3), "n2", rep("n3", times = 5)))
Exécutera trois travailleurs sur n1
, un sur n2
et cinq sur n3
, dans le total de neuf travailleurs parallèles.
Ceci, nous avons discuté de ce qui peut être appelé "topologie plate" des futurs, c'est-à-dire que tous les futurs sont créés et affectés au même environnement. Cependant, rien ne nous empêche d'utiliser une "topologie imbriquée" de futurs, où un ensemble de futurs peut, à son tour, créer un autre ensemble de futures en interne et ainsi de suite.
Par exemple, voici un exemple de deux «meilleurs» futures ( a
et b
) qui utilise une évaluation multi-session et où le deuxième avenir ( b
) utilise à son tour deux futurs internes:
> 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
Par inspection les ID de processus, nous voyons qu'il existe au total trois processus différents impliqués pour résoudre les contrats à terme. Il y a le processus R principal (PID 1437557), et il y a les deux processus utilisés par a
(PID 1437804) et b
(PID 1437805). Cependant, les deux contrats à terme ( b1
et b2
) qui sont imbriqués par b
sont évalués par le même processus R que b
. En effet, les futures imbriquées utilisent une évaluation séquentielle, sauf indication contraire. Il y a plusieurs raisons à cela, mais la principale raison est qu'elle nous protège de la réparation d'un grand nombre de processus de fond par erreur, par exemple via des appels récursifs.
Pour spécifier un type différent de topologie d'évaluation , autre que le premier niveau de contrat à terme résolu par l'évaluation multi-sessions et le deuxième niveau par évaluation séquentielle, nous pouvons fournir une liste de stratégies d'évaluation pour plan()
. Premièrement, les mêmes stratégies d'évaluation que ci-dessus peuvent être explicitement spécifiées que:
plan( list ( multisession , sequential ))
Nous obtiendrions en fait le même comportement si nous essayions avec plusieurs niveaux d'évaluations multi-sessions;
> 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
La raison en est également ici pour nous protéger contre le lancement de plus de processus que ce que la machine peut prendre en charge. En interne, cela se fait en définissant mc.cores = 1
tel que des fonctions comme parallel::mclapply()
se replieront pour s'exécuter séquentiellement. C'est le cas à la fois pour l'évaluation multisesession et multicore.
Poursuivant, si nous commençons par évaluation séquentielle, puis utilisons une évaluation multi-sessions pour tout avenir imbriqué, nous obtenons:
> 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
qui montrent clairement que a
et b
sont résolus dans le processus d'appel (PID 1437557) tandis que les deux futures imbriqués ( b1
et b2
) sont résolus dans deux processus R distincts (PID 1438017 et 1438016).
Cela dit, il est en effet possible d'utiliser des stratégies d'évaluation de multisesse imbriquées, si nous spécifions explicitement ( Force de lecture) le nombre de noyaux disponibles à chaque niveau. Pour ce faire, nous devons "modifier" les paramètres par défaut, qui peuvent être faits comme suit:
> 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
Premièrement, nous voyons que a
et b
sont résolus dans différents processus (PIDS 1438105 et 1438106) que le processus d'appel (PID 1437557). Deuxièmement, les deux contrats à terme imbriqués ( b1
et b2
) sont résolus dans deux autres processus R (PIDS 1438211 et 1438212).
Pour plus de détails sur le travail avec les futurs imbriqués et les différentes stratégies d'évaluation à chaque niveau, voir Vignette «Futures in R: Future Topologies».
Il est possible de vérifier si un avenir a été résolu ou non sans blocage. Cela peut être fait en utilisant la fonction resolved(f)
, qui prend un futur f
explicite comme entrée. Si nous travaillons avec des futurs implicites (comme dans tous les exemples ci-dessus), nous pouvons utiliser la fonction f <- futureOf(a)
pour récupérer le futur explicite à partir d'un avenir implicite. Par exemple,
> 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
Parfois, l'avenir n'est pas ce à quoi vous vous attendiez. Si une erreur se produit lors de l'évaluation d'un avenir, l'erreur est propagée et lancée comme une erreur dans l'environnement d'appel lorsque la valeur future est demandée . Par exemple, si nous utilisons une évaluation paresseuse sur un avenir qui génère une erreur, nous pourrions voir quelque chose comme
> 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
L'erreur est lancée à chaque fois que la valeur est demandée, c'est-à-dire si nous essayons de retrouver la valeur générera la même erreur (et sortie):
> a
Future ' a ' ...
Error in log( b ) : non - numeric argument to mathematical function
In addition : Warning message :
restarting interrupted promise evaluation
Pour voir le dernier appel de la pile d'appels qui a donné l'erreur, nous pouvons utiliser la fonction backtrace()
(*) sur le futur, c'est-à-dire
> backtrace( a )
[[ 1 ]]
log( a )
(*) Le traceback()
ne fournit pas d'informations pertinentes dans le contexte des futures. En outre, il n'est malheureusement pas possible de voir la liste des appels (expressions évaluées) qui ont conduit à l'erreur; Seul l'appel qui a donné l'erreur (ceci est dû à une limitation dans tryCatch()
utilisée en interne).
Chaque fois qu'une expression R doit être évaluée de manière asynchrone (en parallèle) ou séquentiellement via une évaluation paresseuse, les objets globaux (aka "libres") doivent être identifiés et transmis à l'évaluateur. Ils doivent être transmis exactement comme ils étaient au moment où l'avenir a été créé, car, pour une évaluation paresseuse, les globaux peuvent autrement changer entre le moment où il est créé et quand il est résolu. Pour le traitement asynchrone, la raison pour laquelle les globaux doivent être identifiés est qu'ils puissent être exportés vers le processus qui évalue l'avenir.
Le futur package essaie d'automatiser ces tâches autant que possible. Il le fait à l'aide du package Globals, qui utilise l'inspection du code statique pour identifier les variables globales. Si une variable globale est identifiée, elle est capturée et mise à la disposition du processus d'évaluation. De plus, si un global est défini dans un package, alors le global n'est pas exporté. Au lieu de cela, il est assuré que le package correspondant est attaché lorsque l'avenir est évalué. Cela reflète non seulement la configuration de la session R principale, mais il minimise également le besoin d'exportation globaux, ce qui enregistre non seulement la mémoire mais aussi le temps et la bande passante, en particulier lors de l'utilisation de nœuds de calcul distants.
Enfin, il convient de préciser que l'identification des globaux à partir de l'inspection de code statique seule est un problème difficile. Il y aura toujours des cas d'angle où l'identification automatique des globaux échoue afin que les faux globaux soient identifiés (moins de préoccupation) ou que certains des vrais globaux manquent (ce qui entraînera une erreur d'exécution ou éventuellement les mauvais résultats). Vignette 'Futures in R: Les problèmes communs avec les solutions' fournit des exemples de cas communs et explique comment les éviter ainsi que comment aider le package à identifier les globaux ou à ignorer les globaux faussement identifiés. Si cela ne suffit pas, il est toujours possible de spécifier manuellement les variables globales par leurs noms (par exemple globals = c("a", "slow_sum")
) ou en paires de valeurs de nom (par exemple globals = list(a = 42, slow_sum = my_sum)
).
Il y a une limitation avec un avenir implicite qui n'existe pas pour les explicites. Parce qu'un avenir explicite est comme tout autre objet dans R, il peut être attribué n'importe où / à n'importe quoi. Par exemple, nous pouvons en créer plusieurs en boucle et les attribuer à une liste, par exemple
> 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
Ce n'est pas possible à faire lors de l'utilisation de futurs implicites. Cela est dû au fait que l'opérateur d'attribution %<-%
ne peut pas être utilisé dans tous les cas où l'opérateur d'attribution <-
peut être utilisé. Il ne peut être utilisé que pour attribuer des valeurs futures aux environnements (y compris l'environnement d'appel), tout comme le fonctionnement assign(name, value, envir)
. Cependant, nous pouvons attribuer des futurs implicites aux environnements utilisant des indices nommés , par exemple
> 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
Ici as.list(v)
bloque jusqu'à ce que tous les futurs de l'environnement v
soient résolus. Ensuite, leurs valeurs sont collectées et renvoyées en tant que liste régulière.
Si des indices numériques sont nécessaires, les environnements de liste peuvent être utilisés. Les environnements de liste, qui sont implémentés par le package LICENV, sont des environnements réguliers avec des opérateurs de sous-ensemble personnalisés permettant de les indexer, tout comme la façon dont les listes peuvent être indexées. En utilisant des environnements de liste où nous allions autrement utiliser des listes, nous pouvons également attribuer des futurs implicites aux objets de type liste à l'aide d'indices numériques. Par exemple,
> 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
Comme précédemment, as.list(v)
bloque jusqu'à ce que tous les contrats à terme soient résolus.
Pour voir une illustration en direct comment différents types de futurs sont évalués, exécutez la démo Mandelbrot de ce package. Tout d'abord, essayez avec l'évaluation séquentielle,
library( future )
plan( sequential )
demo( " mandelbrot " , package = " future " , ask = FALSE )
Ce qui ressemble à la façon dont le script s'exécuterait si les futurs n'étaient pas utilisés. Ensuite, essayez l'évaluation de la multisesse, qui calcule les différents plans Mandelbrot en utilisant des processus R parallèles en arrière-plan. Essayer,
plan( multisession )
demo( " mandelbrot " , package = " future " , ask = FALSE )
Enfin, si vous avez accès à plusieurs machines, vous pouvez essayer de configurer un groupe de travailleurs et de les utiliser, par exemple
plan( cluster , workers = c( " n2 " , " n5 " , " n6 " , " n6 " , " n9 " ))
demo( " mandelbrot " , package = " future " , ask = FALSE )
R Package Future est disponible sur CRAN et peut être installé dans R en tant que:
install.packages( " future " )
Pour installer la version de pré-libération disponible dans Git Branch develop
sur GitHub, utilisez:
remotes :: install_github( " futureverse/future " , ref = " develop " )
Cela installera le package à partir de la source.
Pour contribuer à ce package, veuillez consulter contribution.md.