Может ли синхронизированный блок с будущим вызвать тупик - PullRequest
0 голосов
/ 12 марта 2020

Скажите, что я делаю следующее:

def foo: Future[Int] = ...

var cache: Option[Int] = None

def getValue: Future[Int] = synchronized {
    cache match {
        case Some(value) => Future(value)
        case None =>
            foo.map { value =>
                cache = Some(value)
                value
            }
    }
}

Есть ли риск тупиковой ситуации с указанным кодом? Или я могу предположить, что синхронизированный блок применяется даже внутри будущего блока карты?

Ответы [ 3 ]

1 голос
/ 12 марта 2020

Обратите внимание, что 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 как кеш?

1 голос
/ 12 марта 2020

Для существования тупика необходимо вызвать как минимум две разные операции блокировки (возможно, в последовательности не по порядку).

Из того, что вы здесь показываете (но мы не видим, что такое реализация foo это), это не тот случай. Существует только одна блокировка, и она реентерабельна (если вы попытаетесь дважды войти в один и тот же блок syncrhronized из одного и того же потока, вы не заблокируете себя).

Следовательно, блокировка невозможна из код, который вы показали.

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

  1. функция, которая может генерировать int
  2. Вы хотите вызвать эту функцию только один раз и кэшировать его результат

Я бы значительно упростил вашу реализацию, если бы это было так:

def expensiveComputation: Int = ???
val foo = Future { expensiveComputation() }
def getValue: Future[Int] = foo

У вас будет один вызов expensiveComputation (для каждого экземпляра окружающего объекта) ) и синхронизированный кеш по его возвращаемому значению, потому что Future сам по себе является безопасным для параллелизма конструктом.

0 голосов
/ 12 марта 2020

Нет, но synchronized на самом деле здесь мало что делает. getValue возвращает почти сразу с Future (который может или не может быть завершен), поэтому блокировка на getValue чрезвычайно недолговечна. Он не ждет, пока foo.map выполнит оценку, прежде чем снимать блокировку, поскольку она выполняется только после завершения foo, что почти наверняка произойдет после возврата getValue.

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