Как выполнить `getOrElseComplete` для `Promise`?

#scala #concurrency #promise #future

#scala #параллелизм #обещание #будущее

Вопрос:

Имеет ли смысл иметь подобную операцию getOrElseComplete , которая пытается завершить a Promise значением, но, если Promise оно уже завершено, вместо этого возвращает существующее завершенное значение. Вот пример реализации:

 implicit class PromiseOps[T](promise: Promise[T]) {
  def getOrElseComplete(value: Try[T]): Try[T] = {
    val didComplete = promise.tryComplete(value)
    if (didComplete) {
      value
    } else {
      // The `tryComplete` returned `false`, which means the promise
      // was already completed, so the following is safe (?)
      promise.future.value.get
    }
  }
}
  

Безопасно ли это делать? Если нет, то почему бы и нет? Если да, есть ли способ сделать это напрямую (например, не полагаясь на сомнительные вещи, такие как _.value.get ) или почему в стандартной библиотеке нет такого способа?

Комментарии:

1. Зачем вам это нужно? Похоже на проблему XY

2. @ghik Я хочу принять одно атомарное решение, и есть несколько разных потоков, которые могут принимать это решение (по-разному) в зависимости от того, что происходит в системе. Затем я также хочу иметь возможность наблюдать за этим решением для завершения. Я думаю, я мог бы сделать это с помощью an AtomicReference и a Promise , но, учитывая, что a Promise уже обертывает an AtomicReference (чтобы быть потокобезопасным), кажется, проще сделать все это с одним объектом promise (меньше внешних инвариантов и меньше кода).

3. Нет причин использовать Promise , если вы не передаете его Future кому-то. Ваш код не показывает, как вы это раскрываете, Future поэтому трудно сказать, как правильно это сделать. Это конкретное решение кажется странным, потому Promise что должно быть только настраиваемым «бэкэндом» a Future , и вы не должны использовать Promise само по себе в качестве источника результирующего значения.

4. Я отдаю его Future кому-то. Вот как выполняется часть «просмотр этого решения для завершения». Я хочу как следить за завершением, так и иногда сразу локально знать, каким было это решение.

Ответ №1:

Из ваших комментариев мне кажется, что это правильное решение вашей проблемы, но я также считаю, что подобный метод не относится к Promise API, потому Promise что предполагается, что он является только настраиваемым «бэкэндом» Future .

Я бы предпочел иметь совершенно отдельную функцию, которая могла бы выглядеть так:

 def getOrComplete[T](promise: Promise[T], value: Try[T]): Try[T] =
  promise.future.value.getOrElse {
    if (promise.tryComplete(value)) value
    else getOrComplete(promise, value)
  }
  

Рекурсивный вызов может показаться странным — он служит двум целям:

  • это защищает от состояния гонки, когда какой-то другой поток завершает future непосредственно перед вызовом tryComplete
  • это позволяет избежать использования .value.get на Future

Возможно, вы также захотите передать value его в качестве параметра по имени, чтобы избежать его оценки, когда Promise он уже завершен.

Комментарии:

1. Очень креативный способ избавиться от ‘get’! Интересно, как заканчивается производительность, учитывая, что даже на счастливом пути вы в конечном итоге делаете два вызова.

2. Если вы действительно хотите избавиться от второго вызова, тогда используйте сопоставление с образцом вместо getOrElse и вызов становится хвостовым рекурсивным, что означает, что он эффективно устраняется в байт-коде.

3. Да, это было бы действительно лучше — спасибо, что указали, что вызов может быть хвостовым рекурсивным.

4. @Alec, будущее из Promise — это тот же объект с другим интерфейсом, поэтому вызов future бесплатный. Я не помню, как это защищено, но, вероятно, с помощью дешевого неоспоримого чтения (я бы предположил, что оно прочитано, если Some (x) возвращает, иначе получает блокировку, чтение, возврат)

Ответ №2:

Эта операция выполняет то, что обещает. Возможно, имеет смысл брать value по имени и не пытаться завершить, если уже завершено, может быть, что-то вроде

 def getOrElseComplete(value: => Try[T]): Try[T] = {
  if (!promise.completed) {
    promise.tryComplete(value)
  }
  promise.future.value.get
}
  

Хотя это довольно хитро. Совместное использование обещания и наличие нескольких мест, где оно может быть завершено, звучит как сложный в обслуживании дизайн, и нужно спросить, что происходит с другим путем, который все еще может выполнить обещание? Разве там что-то не должно быть отменено?

Комментарии:

1. Обещание представляет результат принятия решения, и может быть несколько параллельных путей кода, которые участвуют в гонке для завершения Promise . Пути к коду могут делать что-то вроде tryComplete и использовать результат этого, чтобы атомарно попытаться принять «решение», одновременно проверяя, попал ли туда какой-то другой поток первым. В другом месте я могу посмотреть обещания Future и запланировать некоторую работу, как только решение будет принято.

2. Передача value по имени означает, что иногда value может оцениваться, даже если оно не является значением завершения обещания (например. при выполнении двух вызовов getOrElseComplete ). Само по себе это не проблема, но о ней стоит знать.