Skip to content

Latest commit

 

History

History
281 lines (211 loc) · 17.6 KB

p09-promises-and-futures.md

File metadata and controls

281 lines (211 loc) · 17.6 KB

Глава 9: Promise и Future на практике

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

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

Тип 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].

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

Неблокирующее IO

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

По возможности пользуйтесь Java библиотеками, которые пользуются неблокирующим I/O. Можно воспользоваться напрямую NIO API для Java или библиотекой-надстройкой, такой как Netty. Такие библиотеки также могут обрабатывать большое число запросов с помощью разумно ограниченного в размерах пула потоков.

Разработка такой библиотеки весьма подходящее место для применения Promise.

Блокирующее IO

Иногда у нас нет доступных 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 применяются на практике.

В следующей статье мы отвлечёмся от параллельных вычислений и углубимся в функциональное программирование. Как оно позволяет писать наглядный код пригодный для многократного использования, это справедливо не только для объектно ориентированного программирования.