Что касается Future
работы последовательно и параллельно:
Это немного сложно, потому что Scala Future
спроектирован как нетерпеливый . В различных библиотеках Scala есть некоторые другие конструкции, которые обрабатывают синхронные и асинхронные эффекты, например, cats IO
, Monix Task
, ZIO
et c. которые разработаны по принципу ленивого , и у них нет такого поведения.
Дело в том, что Future
стремится к тому, что он начнет вычисление, как только сможет . Здесь «начало» означает планирование его на ExecutionContext
, который либо выбран явно, либо присутствует неявно. Хотя технически возможно, что выполнение немного остановится в случае, если планировщик решит сделать это, скорее всего, он будет запущен почти мгновенно.
Итак, если у вас есть значение типа Future
, оно будет начать бегать тут же. Если у вас есть ленивое значение типа Future
или функция / метод, возвращающий значение типа Future
, тогда это не так.
Но даже если у вас есть простые значения (нет ленивых значений или defs), если определение Future
выполняется внутри for-computing, то это означает, что оно является частью цепочки monadi c flatMap (если вы этого не понимаете, игнорируйте его пока), и оно будет запущено последовательно , а не параллельно. Зачем? Это не указано с c по Future
s; каждое понимание for имеет семантику последовательной цепочки, в которой вы можете передать результат предыдущего шага следующему шагу. Поэтому логично, что вы не можете запустить что-то на шаге n + 1 , если это зависит от чего-то из шага n .
Вот какой-то код, чтобы продемонстрировать это.
val program = for {
_ <- Future { Thread.sleep(5000); println("f1") }
_ <- Future { Thread.sleep(5000); println("f2") }
} yield ()
Await.result(program, Duration.Inf)
Эта программа подождет пять секунд, затем напечатает «f1», затем подождет еще пять секунд и затем напечатает «f2».
Теперь давайте посмотрим на это:
val f1 = Future { Thread.sleep(5000); println("f1") }
val f2 = Future { Thread.sleep(5000); println("f2") }
val program = for {
_ <- f1
_ <- f2
} yield ()
Await.result(program, Duration.Inf)
Однако программа будет печатать «f1» и «f2» одновременно через пять секунд.
Обратите внимание, что во втором случае семантика последовательности на самом деле не нарушается. f2
по-прежнему имеет возможность использовать результат f1
. Но f2
не использует результат f1
; это отдельное значение, которое может быть вычислено немедленно (определяется с помощью val
). Итак, если мы заменим val f2
на функцию, например def f2(number: Int)
, то выполнение изменится:
val f1 = Future { Thread.sleep(5000); println("f1"); 42 }
def f2(number: Int) = Future { Thread.sleep(5000); println(number) }
val program = for {
number <- f1
_ <- f2(number)
} yield ()
Как и следовало ожидать, через пять секунд будет выведено «f1», и только тогда other Future
start, поэтому он напечатает «42» еще через пять секунд.
Что касается транзакций:
Как @cbley упомянул в комментарии, это звучит так, будто вам нужны транзакции базы данных. Например, в базах данных SQL это имеет очень специфику c, означающую , и обеспечивает свойства ACID .
Если это то, что вам нужно, вам нужно решить это на уровне базы данных. Future
слишком общий c для этого; это просто тип эффекта, моделирующий вычисления syn c и asyn c. Когда вы видите значение Future
, просто взглянув на тип, вы не можете определить, является ли это результатом вызова базы данных или, скажем, какого-то HTTP-вызова.
Например, doob ie описывает каждый запрос к базе данных как тип ConnectionIO
. У вас может быть несколько запросов, выстроенных в линию для понимания, как в случае с Future
:
val program = for {
a <- database.getA()
_ <- database.write("foo")
b <- database.getB()
} yield {
// use a and b
}
Но в отличие от наших предыдущих примеров, здесь getA()
и getB()
не возвращаются значение типа Future[A]
, но ConnectionIO[A]
. Что круто в этом, так это то, что doob ie полностью заботится о том, что вы, вероятно, хотите, чтобы эти запросы выполнялись в одной транзакции, поэтому, если getB()
завершится неудачно, "foo" не будет зафиксировано в базе данных. *
В этом случае вы должны получить полное описание вашего набора запросов, заключить его в одно значение program
типа ConnectionIO
, и как только вы действительно захотите запустить транзакцию, вы должны сделайте что-нибудь вроде program.transact(myTransactor)
, где myTransactor
- это экземпляр Transactor
, doob ie конструкция , которая знает, как подключиться к вашей физической базе данных.
И как только вы совершаете транзакцию, ваш ConnectionIO[A]
превращается в Future[A]
. Если транзакция не удалась, вы получите ошибку Future
, и в вашей базе данных ничего не будет зафиксировано.
Если ваши операции с базой данных не зависят друг от друга и могут выполняться параллельно, doob ie также позволит вам это сделать. Фиксация транзакций через doob ie, как последовательно, так и параллельно, довольно хорошо объясняется в документации .