Обратите внимание, что Future
сам по себе функционирует как кеш (см. Ответ GPI). Однако ответ GPI не совсем эквивалентен вашему коду: ваш код будет только кэшировать успешное значение и будет повторяться, в то время как если исходный вызов expensiveComputation
в ответе GPI не удастся, getValue
всегда будет неудачным.
Это, однако, дает нам повторную попытку до успешного завершения:
def foo: Future[Int] = ???
private def retryFoo(): Future[Int] = foo.recoverWith{ case _ => retryFoo() }
lazy val getValue: Future[Int] = retryFoo()
В общем, все, что связано с Future
s, которое является асинхронным, не будет относиться к блоку synchronized
, если только не произойдет Await
на асинхронная часть в блоке synchronized
(который побеждает точку). В вашем случае абсолютно возможно выполнение следующей последовательности (среди многих других):
- Исходное состояние:
cache = None
- Поток A вызывает
getValue
, получает блокировку - Thread Шаблон соответствует None, вызывает
foo
для получения Future[Int]
(fA0
), планирует обратный вызов для запуска в каком-то потоке B при успешном завершении fA0
(fA1
) - Поток A снимает блокировку
- Поток A возвращает
fA1
- Поток C вызывает
getValue
, получает блокировку - Поток C скороговорка соответствует None, вызывает
foo
для получения Future[Int]
(fC0
), планирует обратный вызов для запуска в каком-то потоке D при успешном завершении fC0
(fC1
) fA0
успешно завершается со значением 42
- Поток B выполняет обратный вызов на
fA0
, устанавливает cache = Some(42)
, успешно завершается со значением 42
- Поток C снимает блокировку
- Поток C возвращает
fC1
fC1
успешно завершается со значением 7
- Поток D выполняет обратный вызов на * 10 62 *, задает
cache = Some(7)
, успешно завершается со значением 7
Приведенный выше код не может зайти в тупик, но нет гарантии, что foo
будет успешно завершен ровно один раз (он может успешно завершиться) произвольно много раз), и нет никаких гарантий относительно того, какое конкретное значение foo
будет возвращено данным вызовом getValue
.
ИЗМЕНИТЬ, чтобы добавить: Вы также можете заменить
cache = Some(value)
value
с
cache.synchronized { cache = cache.orElse(Some(value)) }
cache.get
Что помешает многократному назначению cache
(т. Е. Он всегда будет содержать value
, возвращаемый первым обратным вызовом map
для выполнения в будущем, возвращенном на foo
). Вероятно, это все еще не зашло бы в тупик (я считаю, что если мне придется рассуждать о тупике, мое время, вероятно, будет лучше потрачено на рассуждения о лучшей абстракции), но лучше ли этот сложный / многословный механизм, чем просто использование повторной попытки при неудаче Future
как кеш?