Асинхронное программирование в R. Есть такое!

Опубликовано Опубликовано в рубрике Основная

Существует, мнение, что одним из недостатком в R является отсутствие вменяемой реализации, для выполнения параллельных вычислений (в частности асинхронного выполнения). Кстати, прочитать для общего представления про виды параллельного выполнения программ можно в этом замечательном ответе на Stackoverflow

Тот-же пакет parallel под Windows работать не будет больше чем с 1 ядром процессора.

Однако, периодически в R появляются пакеты, которые совершенно меняют все «правила игры», суть языка, его прикладные возможности. Чтобы понятно о чем я говорю, назову такие пакеты как plyr/dplyr, ggplot2, shiny. И вот, можно представить еще один такой пакет.

Встречайте – пакет future ! (на самом деле – пакету уже больше года…)

Если вы подумали, что future – переводится как будущее – значит вы не очень интересовались до этого асинхронными вычислениями. На самом деле – это слово является частью из триады слов future, promise и delay, которые формируют стратегию вычисления, применяемую для параллельных вычислений.
Ничего не понятно?
Понимаю вас!
Хотите попробовать разобраться – прочитайте об этом в Wikipedia

Но, несмотря на то, что все это кажется сложным – на самом деле интуитивно понять на примерах довольно не сложно. И тогда вы сможете использовать всю мощь своего i7-процессора под Windows.

Итак – как обычно R выполняет команды? Линейно, одну за одной. Ну, понятно с ветвлениями IF и циклами. Но суть от этого не меняется. Одна команда за раз. Если не брать всякие sapply из пакета parallel, про который уже говорилось выше, что под Windows нельзя запустить больше чем в один поток. И потом, sapply – это можно рассматривать как одну команду, и мы вынуждены сидеть и ждать пока она отработает.

Теперь о концепции future. Это по сути — «обещание посчитать выражение».
То есть вы присваиваете переменной команду future – и можете заниматься своим делом, то есть выполнять другие куски кода.
А компьютер с момента подачи команды future – начинает выполнять эти расчеты, но при этом не блокирует процесс выполнения других команд.
Лишь в тот момент, когда вам понадобиться использовать переменную, которой присвоили выражение future – в этом случае придется дожидаться окончания расчетов.

Давайте посмотрим на примере.

Допустим, мы хотим посчитать среднее из огромного числа чисел из нормального распределения
Как это выглядит при обычной работе:
samples <- rnorm(95000000)
mean(samples)


И все это время пока создается массив чисел и считается среднее – мы ничего больше делать не можем. Но ведь у нас еще есть второе и третье и четвертое ядро на процессоре, которые по сути простаивают. Давайте сделаем в этот момент еще кое-что полезное. Например, посчитаем, сколько раз мы сможем увеличить инкремент, пока считается этот пример. На самом деле – идеальный вариант для прогресс-бара при длинных вычислениях. Небольшой комментарий к коду:
Допустим f1 <-future() переменная, которой мы присвоили такое наше выражение future
Тогда мы можем контролировать, сдержала ли она свое обещание посчитать нам что мы просили, или еще нет.
resolved(f1) – вернет TRUE как только выражение будет посчитано. Пока не посчитано будет возвращать FALSE. Есть небольшое замечание – если в процессе вычисления выражения произойдет какая-либо ошибка, например, моя самая любимая – «Cannot allocate memory vector» — то это выражение также может вернуть FALSE. Ведь по сути – этот асинхронный процесс завершен.
А чтобы получить значение, посчитанное с помощью feature надо сказать value(f1). Где f1 – определено выше – переменная которой присвоили процесс feature().
Итак, теперь работающий пример, который поможет вам понять что происходит:
library(future)
# Задаем инкрементную переменную
i <- 0
# Определяем конструкцию future
f1 <- future(
{
# теперь обычный код, как будто обычное выполнение
samples <- rnorm(95000000)
mean(samples)
# завершение обертки future
}) %plan% multiprocess

# Как я обещал - на предыдущем присваивании - ничего не застопорилось,
# мы можем не ожидая его завершения - выполнять другой код
# поэтому, пока не посчиталось - в цикле
# увеличиваем и печатаем инкрементную переменную
while (!resolved(f1))
{
i <- i+1
print(i)
}
# тут мы можем посмотреть, что же там по сути посчиталось
value(f1)

Надеюсь, вы уже догадываетесь – какого могущества мы достигли ?

Где это может быть полезно ?
* Параллельные вычисления. Тут надо подробно писать, пока воздержусь.
* Неблокирующий асинхронный ввод/вывод.
Допустим, вам надо прочитать 4 огромных CSV файла. Кто сказал, что это надо делать последовательно?

library(readr)

df1 <- future({ read_csv("data/csv1.csv") }) %plan% multiprocess
df2 <- future({ read_csv("data/csv2.csv") }) %plan% multiprocess
df3 <- future({ read_csv("data/csv3.csv") }) %plan% multiprocess
df4 <- future({ read_csv("data/csv4.csv") }) %plan% multiprocess

df <- rbind(value(df1), value(df2), value(df3), value(df4))

*Аналогично – это могут быть асинхронные HTTP-запросы, или ODBC-запросы. И прочая-прочая-прочая.

Хочу сказать, у меня нет цели в этом посте создать полный туториал по этому пакету. Я хочу лишь немножно заинтересовать и предложить вам обратить внимание на этот пакет.
Тут рассмотрены лишь примеры повышения быстродействия программ на одном компьютере, с несколькими ядрами, но пакет future намного богаче и содержит также концепции lazy/cluster (для нескольких компьютеров/кластера). Если эта тема вас заинтересовала – добро пожаловать в официальную документацию пакета future !

Асинхронное программирование в R. Есть такое!: 1 комментарий

Добавить комментарий