it-roy-ru.com

Scala асинхронный/ожидание и распараллеливание

Я узнаю об использовании async/await в Scala. Я прочитал это в https://github.com/scala/async

Теоретически этот код является асинхронным (неблокирующим), но он не распараллелен:

def slowCalcFuture: Future[Int] = ...             
def combined: Future[Int] = async {               
   await(slowCalcFuture) + await(slowCalcFuture)
}
val x: Int = Await.result(combined, 10.seconds)    

тогда как этот другой распараллелен:

def combined: Future[Int] = async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

Единственная разница между ними заключается в использовании промежуточных переменных. Как это может повлиять на распараллеливание?

35
Sanete

Так как он похож на async & await в C #, возможно, я смогу дать некоторое представление. В C # существует общее правило, что Task, который можно ожидать, должен быть возвращен как «горячий», т. Е. Уже запущен. Я предполагаю, что то же самое в Scala, где возвращаемая из функции Future не должна явно запускаться, а просто «работает» после вызова. Если это не случай, то следующее является чистой (и, вероятно, не верной) спекуляцией.

Давайте проанализируем первый случай:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
}

Мы добираемся до этого блока и попадаем в первое ожидание:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
    ^^^^^
}

Итак, мы асинхронно ожидаем окончания этих вычислений. Когда он закончен, мы «переходим» к анализу блока:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
                            ^^^^^
}

Второе ожидание, поэтому мы асинхронно ожидаем окончания второго вычисления. После этого мы можем вычислить окончательный результат, добавив два целых числа.

Как видите, мы шаг за шагом продвигаемся вперед, ожидая Futures, когда они приходят один за другим.

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

async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

Итак, вот что (вероятно) происходит:

async {
  val future1 = slowCalcFuture // >> first future is started, but not awaited
  val future2 = slowCalcFuture // >> second future is started, but not awaited
  await(future1) + await(future2)
  ^^^^^
}

Затем мы ожидаем первого Future, но оба фьючерса в данный момент запущены. Когда первый возвращается, второй, возможно, уже завершен (таким образом, у нас будет результат, доступный сразу), или нам, возможно, придется ждать немного дольше.

Теперь ясно, что второй пример выполняет два вычисления параллельно, а затем ожидает завершения обоих. Когда оба готовы, он возвращается. Первый пример запускает вычисления неблокирующим образом, но последовательно.

47
Patryk Ćwiek

ответ Патрика правильный, если немного трудно следовать. Главное, что нужно знать об async/await, это то, что это просто еще один способ сделать Future's flatMap. за кулисами нет волшебства параллелизма. все вызовы внутри асинхронного блока являются последовательными, включая ожидание, которое на самом деле не блокирует исполняющий поток, а заключает остаток асинхронного блока в замыкание и передает его как обратный вызов по завершении of Future мы ждем. поэтому в первом фрагменте кода второе вычисление не начинается до тех пор, пока не закончится первое ожидание, так как до этого его никто не запускал.

22
Mikha

В первом случае вы создаете новый поток для выполнения медленного будущего и ожидаете его за один вызов. Таким образом, вызов второго медленного будущего выполняется после завершения первого.

Во втором случае, когда вызывается val future1 = slowCalcFuture, он эффективно создает новый поток, передает потоку указатель на функцию «slowCalcFuture» и говорит «выполните его, пожалуйста». Требуется столько времени, сколько необходимо для получения экземпляра потока из пула потоков и передачи указателя на функцию в экземпляр потока. Который можно считать мгновенным. Таким образом, поскольку val future1 = slowCalcFuture преобразуется в операции «получить указатель потока и пройти», он завершается в кратчайшие сроки, и следующая строка выполняется без задержки val future2 = slowCalcFuture. Feauture 2 также планируется запустить без каких-либо задержек.

Принципиальное различие между val future1 = slowCalcFuture и await(slowCalcFuture) такое же, как между тем, чтобы попросить кого-нибудь приготовить вам кофе и ждать, пока ваш кофе будет готов. Запрашивание занимает 2 секунды: что нужно, чтобы сказать фразу: «Не могли бы вы сделать мне кофе, пожалуйста?». Но ожидание готового кофе займет 4 минуты.

Возможной модификацией этой задачи может быть ожидание первого доступного ответа. Например, вы хотите подключиться к любому серверу в кластере. Вы отправляете запросы на подключение к каждому известному вам серверу, и первым ответит ваш сервер. Вы можете сделать это с помощью: Future.firstCompletedOf(Array(slowCalcFuture, slowCalcFuture))

0
Vadim Chekan