Jakie są przypadki użycia scala.concurrent.Promise?

93

Czytam SIP-14 i koncepcja Futurema doskonały sens i jest łatwa do zrozumienia. Ale mam dwa pytania dotyczące Promise:

  1. SIP mówi Depending on the implementation, it may be the case that p.future == p. Jak to może być? Czy są Futurei Promisenie są dwoma różnymi typami?

  2. Kiedy powinniśmy użyć Promise? Przykładowy producer and consumerkod:

    import scala.concurrent.{ future, promise }
    val p = promise[T]
    val f = p.future
    
    val producer = future {
        val r = produceSomething()
        p success r
        continueDoingSomethingUnrelated()
    }
    val consumer = future {
        startDoingSomething()
        f onSuccess {
            case r => doSomethingWithResult()
        }
    }
    

jest łatwy do odczytania, ale czy naprawdę musimy tak pisać? Próbowałem to zaimplementować tylko z Future i bez Promise w ten sposób:

val f = future {
   produceSomething()
}

val producer = future {
   continueDoingSomethingUnrelated()
}

startDoingSomething()

val consumer = future {
  f onSuccess {
    case r => doSomethingWithResult()
  }
}

Jaka jest różnica między tym a podanym przykładem i co sprawia, że ​​obietnica jest konieczna?

xiefei
źródło
W pierwszym przykładzie continueDoingSomethingUnrelated () oblicza wartość po createSomething () w tym samym wątku.
senia
1
Aby odpowiedzieć na pytanie nr 1, tak Futurei Promisesą to dwa oddzielne typy, ale jak widać na stronie github.com/scala/scala/blob/master/src/library/scala/concurrent/ ... ta konkretna Promiseimplementacja również się rozszerza Future.
Dylan

Odpowiedzi:

118

Obietnica i przyszłość to koncepcje uzupełniające się. Przyszłość to wartość, która zostanie odzyskana, no cóż, kiedyś w przyszłości i możesz z nią coś zrobić, gdy nastąpi to wydarzenie. Jest to zatem punkt końcowy odczytu lub wyjścia obliczenia - jest to coś, z czego pobierasz wartość.

Obietnica jest analogicznie stroną obliczeń do pisania. Tworzysz obietnicę, która jest miejscem, w którym umieścisz wynik obliczeń i z tej obietnicy otrzymasz przyszłość, która zostanie wykorzystana do odczytania wyniku, który został umieszczony w obietnicy. Kiedy wypełnisz obietnicę, albo przez porażkę, albo sukces, wywołasz wszystkie zachowania, które były związane z powiązaną z nią Przyszłością.

Odnosząc się do twojego pierwszego pytania, jak to możliwe, że mamy obietnicę p p.future == p. Możesz to sobie wyobrazić jako bufor pojedynczego elementu - kontener, który jest początkowo pusty i możesz później przechowywać jedną wartość, która na zawsze stanie się jego zawartością. Teraz, w zależności od twojego punktu widzenia, jest to zarówno obietnica, jak i przyszłość. To obietnica dla kogoś, kto zamierza zapisać wartość w buforze. To przyszłość dla kogoś, kto czeka, aż ta wartość zostanie umieszczona w buforze.

W szczególności w przypadku współbieżnego interfejsu API Scala, jeśli spojrzysz na cechę Promise w tym miejscu , możesz zobaczyć, jak są implementowane metody z obiektu towarzyszącego Promise:

object Promise {

  /** Creates a promise object which can be completed with a value.
   *  
   *  @tparam T       the type of the value in the promise
   *  @return         the newly created `Promise` object
   */
  def apply[T](): Promise[T] = new impl.Promise.DefaultPromise[T]()

  /** Creates an already completed Promise with the specified exception.
   *  
   *  @tparam T       the type of the value in the promise
   *  @return         the newly created `Promise` object
   */
  def failed[T](exception: Throwable): Promise[T] = new impl.Promise.KeptPromise[T](Failure(exception))

  /** Creates an already completed Promise with the specified result.
   *  
   *  @tparam T       the type of the value in the promise
   *  @return         the newly created `Promise` object
   */
  def successful[T](result: T): Promise[T] = new impl.Promise.KeptPromise[T](Success(result))

}

Teraz te implementacje obietnic, DefaultPromise i KeptPromise można znaleźć tutaj . Oba rozszerzają podstawową małą cechę, która ma tę samą nazwę, ale znajduje się w innym opakowaniu:

private[concurrent] trait Promise[T] extends scala.concurrent.Promise[T] with scala.concurrent.Future[T] {
  def future: this.type = this
}

Możesz więc zobaczyć, co mają na myśli p.future == p.

DefaultPromisejest buforem, o którym mówiłem powyżej, podczas gdy KeptPromisejest buforem z wartością wprowadzoną od samego jego utworzenia.

Jeśli chodzi o twój przykład, przyszły blok, którego tam używasz, faktycznie tworzy obietnicę za kulisami. Spójrzmy na definicję futurew tutaj :

def future[T](body: =>T)(implicit execctx: ExecutionContext): Future[T] = Future[T](body)

Postępując zgodnie z łańcuchem metod, znajdziesz się w impl.Future :

private[concurrent] object Future {
  class PromiseCompletingRunnable[T](body: => T) extends Runnable {
    val promise = new Promise.DefaultPromise[T]()

    override def run() = {
      promise complete {
        try Success(body) catch { case NonFatal(e) => Failure(e) }
      }
    }
  }

  def apply[T](body: =>T)(implicit executor: ExecutionContext): scala.concurrent.Future[T] = {
    val runnable = new PromiseCompletingRunnable(body)
    executor.execute(runnable)
    runnable.promise.future
  }
}

Tak więc, jak widać, wynik uzyskany z bloku producenta zamienia się w obietnicę.

PÓŹNIEJSZA EDYCJA :

Odnośnie zastosowania w świecie rzeczywistym: w większości przypadków nie będziesz zajmować się bezpośrednio obietnicami. Jeśli użyjesz biblioteki, która wykonuje obliczenia asynchroniczne, będziesz po prostu pracować z futures zwracanymi przez metody biblioteki. W tym przypadku obietnice są tworzone przez bibliotekę - po prostu pracujesz nad czytaniem tego, co robią te metody.

Ale jeśli chcesz zaimplementować własne asynchroniczne API, musisz zacząć z nimi pracować. Załóżmy, że musisz zaimplementować asynchronicznego klienta HTTP oprócz, powiedzmy, Netty. Wtedy twój kod będzie wyglądał mniej więcej tak

    def makeHTTPCall(request: Request): Future[Response] = {
        val p = Promise[Response]
        registerOnCompleteCallback(buffer => {
            val response = makeResponse(buffer)
            p success response
        })
        p.future
    }
Marius Danila
źródło
3
@xiefei Przypadek użycia Promises powinien znajdować się w kodzie implementacji. Futureto przyjemna, tylko do odczytu rzecz, którą można ujawnić kodowi klienta. Ponadto Future.future{...}składnia może być czasami kłopotliwa.
Dylan
11
Możesz to zobaczyć w ten sposób: nie możesz mieć przyszłości bez obietnicy. Przyszłość nie może zwrócić wartości, jeśli nie ma obietnicy, która została spełniona w pierwszej kolejności. Obietnice nie są opcjonalne, są obowiązkową stroną do pisania w przyszłości. Nie możesz pracować tylko z futures, ponieważ nie byłoby nikogo, kto zapewniłby im wartość zwracaną.
Marius Danila
4
Myślę, że rozumiem, co masz na myśli, mówiąc o zastosowaniach w świecie rzeczywistym: zaktualizowałem moją odpowiedź, aby podać przykład.
Marius Danila
2
@Marius: Biorąc pod uwagę podany przykład ze świata rzeczywistego, co zrobić, jeśli makeHTTPCall jest zaimplementowany w następujący sposób: def makeHTTPCall(request: Request): Future[Response] = { Future { registerOnCompleteCallback(buffer => { val response = makeResponse(buffer) response }) } }
puneetk
1
@puneetk to będziesz mieć przyszłość, która kończy się zaraz po registerOnCompleteCallback()zakończeniu. Poza tym nie wraca Future[Response]. Future[registerOnCompleteCallback() return type]Zamiast tego wraca .
Evgeny Veretennikov