Подтвердить что ты не робот

Scala async/ожидание и распараллеливание

Я изучаю использование 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)
}

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Как вы можете видеть, мы шаг за шагом ждем, ожидая Future, когда они появляются один за другим.

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

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, но оба фьючерса в настоящее время запущены. Когда первый возвращается, второй может быть уже завершен (так что мы получим результат сразу), или нам, возможно, придется подождать немного дольше.

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

Ответ 2

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

Ответ 3

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

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

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

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