В предыдущей главе мы познакомились с типом Future
, его семантикой, тем
как, собирая составные Future
из простых, мы можем писать очень наглядные асинхронные
программы.
В той статье я также упомянул о том, что Future
— это лишь одна сторона медали:
это тип, который позволяет создавать асинхронно вычисляемые значения, которые доступны
только для чтения, также предусмотрен очень элегантный способ обработки исключений.
Но для того чтобы мы могли считать значение из Future
должен быть какой-то другой
механизм, позволяющий это значение записать. В этой статье мы посмотрим как это
делается с помощью типа Promise
. В конце статьи мы посмотрим как Future
и Promise
применяются на практике.
В предыдущей статье мы передавали блок последовательного кода вместе с контекстом
вычисления ExecutionContext
в метод apply
, который волшебным образом выполнял
код асинхронно, возвращая результат в Future
.
Это очень простой метод создания значений типа Future
, но есть и другой метод.
В то время как Future
предоставляет методы только для чтения, тип Promise
позволяет
завершить вычисление Future
записью значения. Значение может быть записано только
один раз. Как только обещание (promise) было исполнено мы не можем его изменить.
Значение типа Promise
всегда связано лишь с одним значением типа Future
.
Если мы присмотримся к типу значение, которое возвращается из метода apply
для объекта Future
, мы сможем заметить, что он также возвращает Promise
:
import concurrent.Future
import concurrent.ExecutionContext.Implicits.global
val f: Future[String] = Future { "Hello world!" }
// REPL output:
// f: scala.concurrent.Future[String] = scala.concurrent.impl.Promise$DefaultPromise@793e6657
Мы получили значение типа DefaultPromise
, который наследует от Future
и Promise
.
Но это лишь деталь реализации. Он может принадлежать к разным значениям Future
и Promise
.
Из этого примера видно, что нет другого способа завершения Future
, кроме как через Promise
.
Это и происходит внутри метода apply
для Future
.
Теперь давайте попробуем поработать с Promise
напрямую.
Одно из первых, что приходит на ум при упоминании обещаний, это политика, выборы, избирательный срок и прочее.
Предположим, что кандидаты пообещали своим избирателям понизить налоги. Мы можем
представить это с помощью значения типа Promise[TaxCut]
, которое можно создать
с помощью метода apply
для объекта Promise
:
import concurrent.Promise
case class TaxCut(reduction: Int)
// необходимо указать тип TaxCut в конструкторе:
val taxcut = Promise[TaxCut]()
// или подсказать тип компилятору при объявлении переменной:
val taxcut2: Promise[TaxCut] = Promise()
Как только было создано значение типа Promise
мы можем получить связанное
с ним значение типа Future
вызовом метода future
на исходном значении:
val taxcutF: Future[TaxCut] = taxcut.future
Возвращённое значение может не совпадать с исходным значением типа Promise
.
Но каждый вызов future
на одном значении Promise
будет возвращать одно и то
же значение типа Future
. Так между значениями Promise
и Future
сохраняется отношение
один к одному.
Если мы пообещали (Promise) всем сделать что-то в ближайшем будущем (Future), нам лучше всего приложить все усилия для того чтобы это произошло. .
В Scala мы можем завершить Promise
либо методом success
либо методом failure
.
Для того чтобы сдержать обещание мы вызываем метод success
на значении Promise
, передав ему
итоговое значение:
taxcut.success(TaxCut(20))
После этого Promise
нельзя изменить и любые попытки приведут к исключениям.
Также вызов метода success
приводит к успешному завершению в принадлежащем данному Promise
значению типа Future
. Будут вызваны все связанные с ним обработчики или функции преобразователи
или фильтры.
Как правило завершение Promise
и обработка Future
будет происходить в разных потоках вычисления.
Скорее всего мы будем делать так: мы создадим значение типа Promise
, начнём вычислять значение
в другом потоке и тут же вернём из функции Future
.
Посмотрим как это происходит на примере:
object Government {
def redeemCampaignPledge(): Future[TaxCut] = {
val p = Promise[TaxCut]()
Future {
println("Starting the new legislative period.")
Thread.sleep(2000)
p.success(TaxCut(20))
println("We reduced the taxes! You must reelect us!!!!1111")
}
p.future
}
}
Пусть вас не смущает вызов метода apply
для Future
в этом примере.
Здесь он используется просто для удобства запуска асинхронных вычислений.
Точно так же я мог бы выполнить вычисление (состоящее в основном из пауз) в Runnable
,
асинхронно выполняющимся в ExecutionContext
, но пришлось бы написать гораздо больше
шаблонного кода. Суть примера в том, что завершение Promise
происходит в отдельном потоке.
Давайте вернёмся к нашей выборной компании и добавим функцию обратного вызова к Future
с помощью метода onComplete
:
import scala.util.{Success, Failure}
val taxCutF: Future[TaxCut] = Government.redeemCampaignPledge()
println("Now that they're elected, let's see if they remember their promises...")
taxCutF.onComplete {
case Success(TaxCut(reduction)) =>
println(s"A miracle! They really cut our taxes by $reduction percentage points!")
case Failure(ex) =>
println(s"They broke their promises! Again! Because of a ${ex.getMessage}")
}
Если выполнить этот код несколько раз ,то станет ясно, что порядок вывода на консоль не определён. Рано или поздно обработчик будет выполнен в успешной альтернативе.
Будучи политиком, Вы уже успели привыкнуть к нарушению обещаний. Будучи Scala-разработчиком,
иногда, у нас просто нет другого выбора. Если это случилось мы можем завершить Promise
вызовом метода failure
с некоторым исключением:
case class LameExcuse(msg: String) extends Exception(msg)
object Government {
def redeemCampaignPledge(): Future[TaxCut] = {
val p = Promise[TaxCut]()
Future {
println("Starting the new legislative period.")
Thread.sleep(2000)
p.failure(LameExcuse("global economy crisis"))
println("We didn't fulfill our promises, but surely they'll understand.")
}
p.future
}
}
Эта реализация метода redeemCampaignPledge
приведёт к нарушению многих обещаний.
Как только мы завершили Promise
вызовом failure
, значение нельзя изменить.
Связанное с ним значение типа Future
также будет завершено с помощью Failure
и будут вызваны все обработчики связанные с альтернативой безуспешного выполнения.
Также мы можем завершить Promise
значением типа Try
, вызвав метод complete
.
Если Try
содержит Success
связанное Future
будет завершено успешно,
иначе — безуспешно.
Если вы хотите повысить масштабируемость вашего приложения за счёт асинхронного
выполнения кода, необходимо чтобы функции на всех уровнях приложения были бы
асинхронными и возвращали Future
.
Скорее всего это понадобиться Вам для построения веб-приложения. Современные
веб-фреймворки на Scala позволяют нам реагировать асинхронно, обработчики
возвращают ответ в виде Future[Response]
, вместо того, чтобы блокировать
поток вычисления и возвращать ответ после того как он будет сформирован.
Это важно. Поскольку благодаря этому веб-сервер сможет ответить на
огромное число запросов с помощью относительно низкого число потоков.
Возвращая Future[Response]
вы позволяете веб-серверу наиболее эффективно
использовать ресурсы пула потоков.
Также любой сервис может делать несколько запросов к базе или другим внешним
веб-сервисам, собирая итоговый результат на основе многих асинхронных результатов.
И для этого мы можем воспользоваться for
-генераторами (как было показано в прошлой статье).
Веб-сервер соберёт такой асинхронный результат в Future[Response]
.
Но как всё это реализовать на практике? Есть три возможных случая.
В нашем приложении скорее всего будет много операций ввода-вывода. К примеру, нам нужно обратиться к базе данных или к сторонним веб серверам.
По возможности пользуйтесь Java библиотеками, которые пользуются неблокирующим I/O. Можно воспользоваться напрямую NIO API для Java или библиотекой-надстройкой, такой как Netty. Такие библиотеки также могут обрабатывать большое число запросов с помощью разумно ограниченного в размерах пула потоков.
Разработка такой библиотеки весьма подходящее место для применения Promise
.
Иногда у нас нет доступных NIO-библиотек. Например большинство драйверов для БД в Java используют блокирующее IO. Если мы сделаем вызов такого драйвера для ответа на HTTP-запрос, это приведёт к тому что вызов будет сделан в потоке веб-сервера. Для избежания этого заключите блок кода для общения с базой в Future::
// вернёт Future[ResultSet] или что-то вроде того:
Future {
queryDB(query)
}
До сих пор мы всегда пользовались неявно определённым глобальным ExecutionContext
для выполнения
блоков кода во Future
. Было бы здорово уметь определять специфический контекст вычисления
для общения с нашей базой данных.
Мы можем создать ExecutionContext
с помощью ExecutionService
из Java. Так мы сможем
настроить наш контекст выполнения специально для базы данных, он не будет зависеть от остального
приложения:
import java.util.concurrent.Executors
import concurrent.ExecutionContext
val executorService = Executors.newFixedThreadPool(4)
val executionContext = ExecutionContext.fromExecutorService(executorService)
В зависимости от назначения приложения иногда нам нужно выполнить задачу, совсем не связанную
с вводом-выводом, которая вычисляется очень долго. Она требует много ресурсов CPU. Не стоит выполнять
такие задачи в потоке веб-сервера. Для этого необходимо обернуть её в Future
:
Future {
longRunningComputation(data, moreData)
}
Также хорошо позаботиться о том, чтобы для таких задач, нагружающих CPU, был определён
отдельный ExecutionContext
. Как настроить ExecutionContext
зависит от приложения,
обсуждение этого вопроса выходит за рамки данной статьи.
В этой статье мы познакомились с типом Promise
, что отвечает за запись значений в Future
.
Также мы узнали о том как Promise
и Future
применяются на практике.
В следующей статье мы отвлечёмся от параллельных вычислений и углубимся в функциональное программирование. Как оно позволяет писать наглядный код пригодный для многократного использования, это справедливо не только для объектно ориентированного программирования.