Dans le dernier article, j'ai parlé de ce qu'est le multithreading et donné des exemples de son implémentation dans le langage R lors de l'utilisation de l'API Yandex.Direct en utilisant les doSNOW
, doParallel
et la construction foreach
.
Cet article est une continuation, mais peut être considéré comme un guide hors ligne du multithreading dans R. J'ai été invité à l'écrire par les commentaires reçus dans la première partie (ici un merci spécial à Alexey_mosc , SatCat , Ananiev_Genrih ), dans lequel on m'a donné un certain nombre de packages qui représentent une approche plus moderne de les implémentations du multithreading dans R, nous en parlerons plus tard.

Table des matières
Défi
À titre d'exemple, nous prenons le problème considéré dans une publication précédente , à savoir en mode multi-thread, collectez une liste de mots clés à partir de 4 comptes publicitaires Yandex.Direct.
Pour travailler avec l'API Yandex.Direct, nous utiliserons le package ryandexdirect
. La documentation officielle pour cela est sur le lien , mais pour la mise en œuvre de la tâche décrite, nous n'avons besoin que de 2 fonctions:
yadirAuth
- autorisation dans l'API Yandex.Direct;yadirGetKeyWords
- Téléchargez une liste de mots clés à partir des comptes publicitaires.
Ce n'est pas seulement que j'ai choisi le processus de téléchargement des mots-clés, le fait est que c'est l'une des opérations les plus longues de l'API Yandex.Direct. Deuxièmement, dans tous les comptes, le nombre de mots-clés est différent, par conséquent, le temps pour terminer cette opération pour chaque compte sera très différent, dans notre cas de 1 à 20 secondes.
La préparation
Au départ, vous devez installer tous les packages décrits dans cet article, pour cela, vous pouvez utiliser le code ci-dessous.
Code 1: installation de packages # install.packages("ryandexdirect") install.packages("tictoc") install.packages("rbenchmark") install.packages("dplyr") install.packages("purrr") install.packages("future") install.packages("promises") install.packages("furrr") install.packages("future.apply")
Pour que les fonctions du package soient disponibles, vous devez le connecter à l'aide de la commande de library
. Pour plus de commodité, je connecterai séparément tous les packages nécessaires dans chaque exemple de code donné.
Nous créons un vecteur composé de connexions Yandex.Direct, à partir desquelles nous demanderons plus tard des mots clés:
Code 2: création d'un vecteur de connexion logins <- c("login1", "login2", "login3", "login4")
Pour travailler avec l'API Yandex.Direct, vous devez d'abord passer par une autorisation sous chaque compte, pour cela, vous pouvez utiliser la conception suivante:
Code 3: autorisation dans l'API Yandex.Direct lapply(logins, function(l) { yadirAuth(Login = l)})
Après avoir exécuté le code ci-dessus, un navigateur s'ouvrira pour autorisation sous chaque compte. Vous confirmez l'autorisation pour ryandexdirect
d'accéder à votre matériel publicitaire. Vous serez redirigé vers la page où vous devez copier le code de vérification. En l'entrant dans la console R, terminez le processus d'autorisation. Cette opération est répétée pour chaque connexion que vous avez spécifiée lors de la création des connexions vectorielles.
Certains utilisateurs, au cours du processus d'autorisation, peuvent être déroutés par le fait d'une redirection vers une ressource tierce, mais il n'y a aucun danger pour votre compte à ce sujet, j'ai décrit ce sujet plus en détail dans l'article "À quel point il est sûr d'utiliser des packages R pour travailler avec des API de systèmes publicitaires" .
Ensuite, nous considérerons plusieurs exemples de mise en œuvre de la tâche décrite. Chacun commencera par un exemple de code et son explication supplémentaire. Je pense que cette option sera la plus pratique pour la perception.
Exemple de solution de traitement en série, fonction sapply et package purrr

Dans le dernier article , j'ai donné un exemple en utilisant une boucle for
comme exemple. Puisque nous avons envisagé le multithreading en utilisant le package foreach
, dont la syntaxe ressemble à des boucles, cet exemple était approprié ici, bien que l'utilisation de boucles ne soit pas bien accueillie par les utilisateurs de R.
Les packages que nous considérerons dans cet article rappellent davantage les fonctions de la famille apply dans la syntaxe; je vais donc donner un exemple de solution en mode série en les utilisant.
Fonction sapply
Pour estimer le temps d'exécution des commandes, dans chacune des approches considérées, nous utiliserons le package tictoc
.
Code 4: Exemple de solution en mode séquentiel utilisant la fonction sapply library(tictoc) library(dplyr) tic() # kw.sapply <- sapply( logins, # , function(x) # # { yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = FALSE # ) toc() # # result.sapply <- do.call("rbind", kw.sapply)
39.36 sec elapsed
: 39.36 sec elapsed
Au début, la syntaxe des fonctions de la famille apply
n'est pas aussi facile à lire que la syntaxe des boucles, mais en fait tout est assez simple.
sapply(X, FUN)
Où:
- X - Un objet dont nous allons parcourir les éléments et les utiliser tour à tour à chaque itération, dans une boucle
for
il ressemblait à ceci: for(i in X)
; - FUN - Une fonction dans laquelle nous substituerons chaque élément de l'objet X à son tour, si nous faisons une analogie avec
for
, c'est le corps de la boucle.
Dans l'exemple de code 4 , le vecteur de connexion créé précédemment est passé à l'argument X. Chaque élément du vecteur de connexion est transmis à son tour comme seul argument à la fonction de function(x) { yadirGetKeyWords(Login = x) %>% mutate(login = x) }
anonyme function(x) { yadirGetKeyWords(Login = x) %>% mutate(login = x) }
qui a été transmise à l'argument FUN .
C'est-à-dire sapply
exécutera la fonction spécifiée dans FUN 4 fois, en y substituant les connexions une par une, et renverra le résultat sous la forme d'une liste (objet de liste de classe) composée de 4 éléments. Chaque élément est un tableau avec une liste de mots clés reçus du compte à chaque itération.
yadirGetKeyWords(Login = "login1") %>% mutate(login = "login1")
yadirGetKeyWords(Login = "login2") %>% mutate(login = "login2")
yadirGetKeyWords(Login = "login3") %>% mutate(login = "login3")
yadirGetKeyWords(Login = "login4") %>% mutate(login = "login4")
L'objet obtenu à l'aide de sapply
a la structure suivante:
summary(kw.sapply)
Length Class Mode login1 19 data.frame list login2 19 data.frame list login3 19 data.frame list login4 19 data.frame list
À la fin de cet exemple, la commande result.sapply <- do.call("rbind", kw.sapply)
combine les 4 éléments de la liste kw.sapply en un seul cadre result.sapply .
# A tibble: 6,804 x 1 result.sapply$Id $Keyword $AdGroupId $CampaignId $ServingStatus $State <dbl> <fct> <dbl> <int> <fct> <fct> 1 15164230566 ~ 3597453985 39351725 ELIGIBLE ON 2 15164230567 ~ 3597453985 39351725 ELIGIBLE ON 3 15164230568 ~ 3597453985 39351725 ELIGIBLE ON 4 15164230569 ~ 3597453985 39351725 ELIGIBLE ON 5 15164230570 ~ 3597453985 39351725 ELIGIBLE ON 6 15164230571 ~ 3597453985 39351725 ELIGIBLE ON 7 15164230572 ~ 3597453985 39351725 ELIGIBLE ON 8 15164230573 ~ 3597453985 39351725 ELIGIBLE ON 9 15164230574 ~ 3597453985 39351725 ELIGIBLE ON 10 15164230575 ~ 3597453985 39351725 ELIGIBLE ON # ... with 6,794 more rows, and 13 more variables: $Status <fct>, # $StrategyPriority <fct>, $StatisticsSearchImpressions <int>, # $StatisticsSearchClicks <int>, $StatisticsNetworkImpressions <int>, # $StatisticsNetworkClicks <lgl>, $UserParam1 <chr>, $UserParam2 <chr>, # $ProductivityValue <lgl>, $ProductivityReferences <lgl>, $Bid <dbl>, # $ContextBid <dbl>, $login <chr>
En plus de sapply
, la famille de fonctions *apply
comprend: apply
, lapply
, vapply
, mapply
et autres.
Forfait purrr
Code 5: Exemple de solution utilisant les fonctions du package purrr library(purrr) library(dplyr) library(tictoc) tic() # result.purrr <- map_df( logins, # , ~ # function(.x) { yadirGetKeyWords(Login = .x) %>% mutate(login = .x) } ) toc() #
35.46 sec elapsed
: 35.46 sec elapsed
Le paquet purrr
fait partie du noyau de la bibliothèque tidyverse
, créée par Headley Wickham.
En termes de sens et de syntaxe, les principales fonctions du package sont très similaires à sapply
, son principal avantage est le suivant:
- Les fonctions sont divisées en familles
map
, map2
, pmap
, walk
etc., des fonctions distinctes incluses dans la même famille renvoient le résultat dans différents formats: chr , dbl , int , df , etc.; - Les fonctions de la famille
map2
vous map2
d' map2
sur des éléments (itérer) simultanément de deux objets; - Les fonctions de la famille
pmap
vous pmap
d' pmap
simultanément sur des éléments d'un nombre quelconque d'objets. Vous pouvez passer une table à l'entrée de l'argument .l (un analogue de l'argument X dans sapply) , dont chaque colonne contiendra les valeurs par lesquelles vous itérerez, et qui seront à leur tour substituées dans les arguments de la même fonction passée en .f (l'analogue FUN de sévèrement) .
Dans quelle situation devons-nous parcourir les éléments de plusieurs objets. Par exemple, vous travaillez avec plusieurs comptes d'agent et les comptes publicitaires à partir desquels vous souhaitez obtenir une liste de mots clés sont dispersés entre eux. Dans ce cas, vous pouvez créer un vecteur à partir des noms des comptes d'agent et le parcourir, en parallèle avec la façon dont vous triez les connexions des comptes publicitaires.
Code 6: Exemple de travail avec plusieurs comptes d'agent library(purrr) # agencies <- c("agency1", NA, "agency2", "agency1") # # result.pmap2 <- map2_df(.x = logins, .y = agencies, ~ { yadirGetKeyWords(Login = .x, AgencyAccount = .y) %>% mutate(login = .x) })
Imaginez maintenant la situation où lorsque vous vous êtes connecté sous différents comptes, vous avez enregistré le fichier avec les informations d'identification dans différents dossiers, puis vous devez itérer immédiatement sur trois objets: les connexions des comptes publicitaires, les connexions des comptes d'agent, le chemin dans lequel le fichier avec les informations d'identification est stocké. Cela peut être fait avec de l'aide. pmap
famille pmap
.
Code 7: exemple de fonction pmap library(purrr) # , # TokenPath <- c("C:\\proj1\\tokens", "C:\\yandex\\token", "C:\\yandex\\token", "C:\\my_yandex_acoount") # pmap.result <- pmap_df(list(Login = logins, AgencyAccount = agencies, TokenPath = TokenPath), yadirGetKeyWords)
Par conséquent, le résultat de l'exécution des fonctions map_df
, map2_df
et pmap_df
est le cadre de date, et lors de leur utilisation, la dernière étape de l'exemple avec sapply
( do.call("rbind", kw.sapply)
) n'est pas requise.
Le code est devenu plus compact et exécuté un peu plus rapidement, mais néanmoins, les deux approches décrites, sapply
et purrr
, collectent séquentiellement les mots clés de chaque compte. Par conséquent, le temps d'exécution total de cette opération est égal à la somme des durées de collecte des données des quatre comptes.
Heure [total] = Heure [connexion1] + Heure [connexion2] + Heure [connexion3] + Heure [connexion4]
Options multithread pour résoudre la tâche de collecte de mots clés à partir de Yandex.Direct

Donc, si vous avez déjà lu le premier article , vous savez que le mode de fonctionnement multithread possède plusieurs fonctionnalités:
- Chaque thread démarre dans une session R distincte avec un environnement de travail propre.
- Pour la même raison, dans un processus en cours d'exécution séparé, les paquets précédemment connectés ne sont pas transmis par défaut.
L'exportation d'objets créés dans un environnement de travail et la connexion de packages dans chaque approche sont implémentées différemment, nous les examinerons plus en détail.
Paquet parallel
Ce package a d'abord été inclus dans le package R dans la version 2.14.0 et est à ce jour livré avec R lui-même.
Code 8: Exemple de solution au problème via le package parallèle library(parallel) library(tictoc) # cl <- makeCluster(4) # clusterExport(cl = cl, varlist = "logins") # , # , ryandexdirect clusterEvalQ(cl = cl, { library(ryandexdirect) library(dplyr) } ) tic() # parallel.kw <- parSapplyLB(cl = cl, # X = logins, # , FUN = function(x) { # # X yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = F) # toc() # # stopCluster(cl) # result.parallel <- dplyr::bind_rows(parallel.kw)
16.75 sec elapsed
: 16.75 sec elapsed
Essayons d'analyser le code 8 . La fonction makeCluster
crée un cluster de 4 processus. Nous pouvons exporter des objets de notre environnement de travail principal vers le cluster créé en utilisant la fonction clusterExport
, pour cela nous devons utiliser ses arguments:
- cl - Cluster dans lequel nous allons exporter des objets
- varlist - Un vecteur de texte contenant les noms des objets à exporter vers chaque processus de cluster.
Une façon de connecter les bons packages sur chaque nœud de cluster consiste à utiliser la fonction clusterEvalQ
. Dans notre exemple, nous l'utilisons pour connecter des packages, mais vous pouvez écrire n'importe quel code R dans clusterEvalQ
, et il sera lancé au début de chaque nœud de cluster. Les arguments pour cette fonction sont assez évidents, vous devez spécifier le cluster et les commandes qui y seront exécutées.
parSapplyLB
est une version parallèle de la fonction sapply
avec équilibrage de charge entre les nœuds de cluster, ils l'utilisent également, mais vous devez spécifier le cluster avec l'argument cl .
Également en parallel
il existe d'autres versions parallélisées des fonctions de la famille *apply
: parLapply
, parSapply
, parApply
, etc.
parSapply
diffère de parSapplyLB
uniquement en ce qu'il n'a pas d'équilibrage de charge sur les nœuds de cluster.
La fonction stopCluster
est utilisée pour arrêter le cluster créé.
La dernière commande, dplyr::bind_rows(parallel.kw)
nous combinons l'objet parallel.kw obtenu en utilisant parSapplyLB
dans une table.
Pour Linux, le parallel
a des fonctions distinctes: mclapply
, mcmapply
, mcMap
. Souvent, dans ce système d'exploitation, les commandes sont exécutées plus rapidement et le code devient plus compact.
Code 9: solution utilisant mclapply pour Linux library(parallel) library(tictic) library(dplyr) library(ryandexdirect) tic() mclapply.kw <- mclapply(logins, FUN = function(x) { # # X yadirGetKeyWords(Login = x) %>% mutate(login = x) }, mc.cores = 4) toc()
Lorsque vous utilisez ces fonctions, il n'est pas nécessaire de démarrer le cluster à l'aide de la makeCluster
. le nombre de nœuds que vous spécifiez à l'aide de l'argument mc.cores . Il n'est pas non plus nécessaire de connecter des packages et d'exporter des objets; ces opérations sont effectuées automatiquement.
Forfait future
L'une des approches les plus modernes de la programmation asynchrone en R.
Un code qui, en parallèle, résoudra notre problème avec l'aide du future
est suffisamment compliqué à comprendre. Par conséquent, analysons son travail sur un exemple plus simple, nous demanderons une liste de mots clés à partir d'un compte.
Code 10: l'exemple le plus simple d'utilisation du futur package library(future) # plan(multiprocess) # # future.kw <- future({yadirGetKeyWords(Login = logins[4])}, packages = "ryandexdirect", globals = "logins") # resolved(future.kw) # future.result.1 <- value(future.kw)
Essayons de comprendre l'exemple du Code 10 . La fonction plan
vous permet de définir et de modifier le mode d'exécution des expressions données, en voici les principales:
- séquentiel - Il s'agit du mode de fonctionnement R habituel; les commandes sont exécutées séquentiellement dans la session en cours;
- multisession - Mode parallèle, les commandes seront exécutées dans les sessions en cours en arrière-plan sur la machine actuelle, tandis que votre session de travail ne sera pas bloquée;
- cluster - Mode parallèle, les commandes seront exécutées sur la machine actuelle ou distante, similaire à la façon dont elle est implémentée dans le package
parallel
.
L'ensemble du future
package est basé sur l'exécution de commandes dans des processus en arrière-plan sans bloquer la session en cours. L'exécution de commandes suit la fonction du même nom future
, donc lorsque nous exécutons la commande:
future({yadirGetKeyWords(Login = logins[4])}, packages = "ryandexdirect", globals = "logins")
Notre session actuelle dans R n'est pas bloquée et la commande est exécutée en arrière-plan, en exécutant une autre session R.
Vous pouvez vérifier l'état actuel du processus d'exécution d'une expression donnée à l'aide de la fonction resolved
. Enfin, la fonction de value
est utilisée pour obtenir le résultat d'une future
exécution. Si vous exécutez la fonction de value
plus tôt que votre future
exécution dans une session d'exécution parallèle, la session de travail en cours sera bloquée jusqu'à la fin de l'expression de session parallèle.
L'exemple de travail le plus avancé est l'utilisation du future
conjointement avec les promises
.
Code 11: Exemple de partage de packages de «futures» et de «promesses» library(future) library(promises) # plan(multiprocess) # # future.kw <- future({suppressMessages( yadirGetKeyWords(Login = logins[4]))}, packages = "ryandexdirect", globals = "logins") %...>% # future, # nrow() %...>% paste("words loaded") %...>% print()
Le package promises
fournit un ensemble d'opérateurs de pipelines qui complètent parfaitement les fonctionnalités future
.
Dans l'exemple Code 11 , en arrière-plan, nous commençons le processus de téléchargement de mots clés à partir d'un compte publicitaire. De plus, l'opérateur de pipeline %...>%
sans bloquer la session de travail attend la fin du future
et effectue les opérations restantes. À la suite de l'exécution du code, à la fin des travaux future
, le nombre de mots-clés du compte spécifié sera affiché dans la console:
[1] "1855 words loaded"
À la fin de l'article, un exemple plus illustratif d'un tas d' future
et de promises
sera démontré.
Par défaut, le future
package exporte lui-même l'intégralité de l'espace de travail vers chaque session parallèle, mais vous pouvez vous-même spécifier une liste d'objets à exporter à l'aide de l'argument global .
Pour connecter les packages à l' future
passer un vecteur contenant leurs noms à l'argument packages .
Revenons maintenant à notre tâche, l'exemple de code suivant en mode parallèle chargera une liste de mots clés à partir de 4 comptes:
Code 12: un exemple de résolution d'un problème à l'aide du futur package library(future) library(tictoc) # plan("multisession", workers = 4) tic() # futs <- lapply(logins, # function(i) # # future({ yadirGetKeyWords(Login = i) %>% mutate(login = i) }, packages = c("ryandexdirect", "dplyr"))) completed <- sapply(futs, resolved) # kw <- lapply(futs, value) # toc() # # result.future <- dplyr::bind_rows(kw)
Délai: 14.83 sec elapsed
Pour télécharger une liste de mots clés en mode multithread à partir de tous les comptes publicitaires répertoriés dans les connexions vectorielles , vous devez exécuter un future
distinct en arrière-plan. Dans l'exemple de code 12, nous implémentons cela en utilisant la fonction lapply
.
Le résultat de travailler lapply
est une liste de future
lancés. Vous pouvez vérifier le statut de chacun en utilisant la commande sapply(futs, resolved)
, qui renverra un vecteur logique où TRUE signifiera que le future
rempli et FALSE que le future
est actuellement en cours.
Pour obtenir des résultats de chaque future
, une fois leur travail terminé, nous utilisons la commande lapply(futs, value)
.
: result.future <- dplyr::bind_rows(kw)
.
future
, (
future
), .
future.apply
future.apply
future
, .
13: future.apply library(future.apply) library(tictoc) # plan("multisession", workers = 4) tic() # kw.future.apply <- future_sapply(logins, # , function(x) { # yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = FALSE, # # future.packages = c("ryandexdirect", "dplyr"), future.globals = TRUE ) toc() #
: 17.28 sec elapsed
13 , future.apply
future
, .
4 : plan("multisession", workers = 4)
.
future_sapply
logins . C'est-à-dire , , sapply
, .
future_sapply
future.packages . future.globals . , .
furrr
future
. purrr
, furrr
.
14: furrr library(furrr) library(tictoc) # cl <- parallel::makeCluster(4) plan(cluster, workers = cl) tic() # furrr.kw <- future_map(logins, ~ # function(.x) yadirGetKeyWords(Login = .x) %>% mutate(login = .x), .options = future_options(packages = c("ryandexdirect", "dplyr"), globals = c())) toc() # # result.furrr <-dplyr::bind_rows(furrr.kw)
: 15.45 sec elapsed
furrr
purrr
. purrr
, .
.options . .options future_options
, .
14 packages globals :
.options = future_options(packages = c("ryandexdirect", "dplyr"), globals = c())
rbenchmark
.
, future
promises
. .
, 20 4 () .
= (T[1] * 20) + (T[2] * 20) + (T[N] * 20)
15: future promises library(furrr) library(parallel) library(dplyr) library(future) library(ryandexdirect) library(tictoc) library(rbenchmark) # logins <- c("login1", "login2", "login3", "login4") # # par par.furrr <- function(logins) { cl <- parallel::makeCluster(4) plan(cluster, workers = cl) furrr.kw <- future_map(logins, ~ yadirGetKeyWords(Login = .x) %>% mutate(login = .x), .options = future_options(packages = c("ryandexdirect", "dplyr"), globals = c())) result.furrr <-dplyr::bind_rows(furrr.kw) } par.future <- function(logins) { plan("multisession", workers = 4) futs <- lapply(logins, function(i) future({ yadirGetKeyWords(Login = i) %>% mutate(login = i) }, packages = c("ryandexdirect", "dplyr"))) completed <- sapply(futs, resolved) kw <- lapply(futs, value) result.future <- dplyr::bind_rows(kw) } par.future.apply <- function(logins) { plan("multisession", workers = 4) kw.future.apply <- future_sapply(logins, function(x) { yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = FALSE, future.packages = c("ryandexdirect", "dplyr"), future.globals = TRUE ) result.future.apply <- dplyr::bind_rows(kw.future.apply) } par.parallel <- function(logins) { cl <- parallel::makeCluster(4) clusterExport(cl = cl, varlist = "logins") clusterEvalQ(cl = cl, { library(ryandexdirect) library(dplyr) } ) parallel.kw <- parSapplyLB(cl = cl, X = logins, FUN = function(x) { yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = F) stopCluster(cl) result.parallel <- dplyr::bind_rows(parallel.kw) } # seq seq.apply <- function(logins) { kw.sapply <- sapply( logins, function(x) { yadirGetKeyWords(Login = x) %>% mutate(login = x) }, simplify = FALSE ) result.sapply <- do.call("rbind", kw.sapply) } seq.purrr <- function(logins) { kw.purrr <- map_df( logins, ~ { yadirGetKeyWords(Login = .x) %>% mutate(login = .x) } ) result.purrr <- do.call("rbind", kw.purrr) } # rbenchmark # future + promises # , # plan(list(tweak(multisession, workers = 2), tweak(multisession, workers = 4))) tic() speed.test <- future({ # within(benchmark(furrr = par.furrr(logins), future = par.future(logins), future.apply = par.future.apply(logins), parallel = par.parallel(logins), apply = seq.apply(logins), purrr = seq.purrr(logins), replications = c(20), columns = c('test', 'replications', 'elapsed'), order = c('elapsed', 'test')), { average = round(elapsed/replications, 2) }) }, packages = c("dplyr", "ryandexdirect", "rbenchmark", "parallel", "purrr", "future", "promises", "furrr", "future.apply"), globals = c("logins", "par.furrr", "par.future", "par.future.apply", "par.parallel", "seq.apply", "seq.purrr")) %...>% print() %...T>% toc() message("My Session is not blocked")
3370 , .. .
. , future
, promises
, .
, . "My Session is not blocked", , , .. .
promises
:
%...>%
— %>%
, . C'est-à-dire , resolved
, future
, value
. , print
.%...T>%
— %T>%
, , . , , , .. .. print
, , .- %...T!% — .
15 plan
tweak
( plan(list(tweak(multisession, workers = 2), tweak(multisession, workers = 4)))
), , 2 , future
4 .
:
My Session is not blocked test replications elapsed average 4 parallel 20 393.02 19.65 1 furrr 20 402.09 20.10 2 future 20 431.19 21.56 3 future.apply 20 432.29 21.61 5 apply 20 847.77 42.39 6 purrr 20 864.19 43.21 3370.55 sec elapsed

, parallel
, . furrr
, future
future.apply
.
1 , , . , API . .
, 4 , .
Conclusion
R, API.
, API . " R , 1" .
:
- doSNOW / doParallel + foreach
- future + promises
- future.apply / furrr
- parallel
, , .
, R .