Quels sont les cas d'utilisation de scala.concurrent.Promise?

93

Je lis SIP-14 et le concept de Futureest parfaitement logique et facile à comprendre. Mais avez deux questions sur Promise:

  1. Le dit SIP Depending on the implementation, it may be the case that p.future == p. Comment se peut-il? Sont Futureet Promisenon deux types différents?

  2. Quand devrions-nous utiliser un Promise? L'exemple de producer and consumercode:

    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()
        }
    }

est facile à lire mais avons-nous vraiment besoin d'écrire comme ça? J'ai essayé de l'implémenter uniquement avec Future et sans Promise comme ceci:

val f = future {
   produceSomething()
}

val producer = future {
   continueDoingSomethingUnrelated()
}

startDoingSomething()

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

Quelle est la différence entre cet exemple et l'exemple donné et qu'est-ce qui rend une promesse nécessaire?

Xiefei
la source
Dans le premier exemple continueDoingSomethingUnrelated () évalue après produireSomething () dans le même thread.
senia
1
Pour répondre à la question n ° 1, oui Futureet il Promiseexiste deux types distincts, mais comme vous pouvez le voir sur github.com/scala/scala/blob/master/src/library/scala/concurrent/... cette Promiseimplémentation particulière s'étend Futureégalement.
Dylan

Réponses:

118

La promesse et l'avenir sont des concepts complémentaires. L'avenir est une valeur qui sera récupérée, enfin, dans le futur et vous pouvez en faire des choses lorsque cet événement se produit. C'est donc le point final de lecture ou de sortie d'un calcul - c'est quelque chose à partir duquel vous récupérez une valeur.

Une promesse est, par analogie, le côté écriture du calcul. Vous créez une promesse qui est l'endroit où vous placerez le résultat du calcul et à partir de cette promesse, vous obtenez un avenir qui sera utilisé pour lire le résultat qui a été mis dans la promesse. Lorsque vous accomplirez une promesse, que ce soit par échec ou par succès, vous déclencherez tout le comportement qui était attaché au futur associé.

Concernant votre première question, comment se fait-il que pour une promesse p que nous ayons p.future == p. Vous pouvez imaginer cela comme un tampon à un seul élément - un conteneur initialement vide et vous pouvez ensuite stocker une valeur qui deviendra son contenu pour toujours. Maintenant, selon votre point de vue, c'est à la fois une promesse et un avenir. C'est prometteur pour quelqu'un qui a l'intention d'écrire la valeur dans le tampon. C'est un avenir pour quelqu'un qui attend que cette valeur soit mise dans le tampon.

Plus précisément, pour l'API simultanée Scala, si vous examinez le trait Promise ici, vous pouvez voir comment les méthodes de l'objet compagnon Promise sont implémentées:

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))

}

Maintenant, ces implémentations de promesses, DefaultPromise et KeptPromise peuvent être trouvées ici . Ils étendent tous les deux un petit trait de base qui porte le même nom, mais il est situé dans un package différent:

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

Vous pouvez donc voir ce qu'ils veulent dire p.future == p.

DefaultPromiseest le tampon KeptPromiseauquel je faisais référence ci-dessus, tandis que c'est un tampon avec la valeur mise depuis sa création même.

En ce qui concerne votre exemple, le futur bloc que vous y utiliserez crée en fait une promesse dans les coulisses. Le regard Let la définition de futuredans ici :

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

En suivant la chaîne de méthodes, vous vous retrouvez dans le futur impl .

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
  }
}

Ainsi, comme vous pouvez le voir, le résultat que vous obtenez de votre bloc de producteurs est versé dans une promesse.

MODIFICATION PLUS TARD :

En ce qui concerne l'utilisation dans le monde réel: la plupart du temps, vous ne traitez pas directement les promesses. Si vous utilisez une bibliothèque qui effectue un calcul asynchrone, vous ne travaillerez qu'avec les futurs retournés par les méthodes de la bibliothèque. Les promesses sont, dans ce cas, créées par la bibliothèque - vous travaillez simplement avec la fin de lecture de ce que font ces méthodes.

Mais si vous devez implémenter votre propre API asynchrone, vous devrez commencer à travailler avec elles. Supposons que vous ayez besoin d'implémenter un client HTTP asynchrone au-dessus de, disons, Netty. Ensuite, votre code ressemblera un peu à ceci

    def makeHTTPCall(request: Request): Future[Response] = {
        val p = Promise[Response]
        registerOnCompleteCallback(buffer => {
            val response = makeResponse(buffer)
            p success response
        })
        p.future
    }
Marius Danila
la source
3
@xiefei Le cas d'utilisation de Promises doit être dans le code d'implémentation. Futureest une bonne chose en lecture seule que vous pouvez exposer au code client. De plus, la Future.future{...}syntaxe peut parfois être lourde.
Dylan
11
Vous pouvez le voir ainsi: vous ne pouvez pas avoir d'avenir sans promesse. Un futur ne peut pas renvoyer une valeur s'il n'y a pas de promesse qui se termine en premier lieu. Les promesses ne sont pas facultatives, elles sont le côté écrit obligatoire d'un futur. Vous ne pouvez pas travailler uniquement avec des contrats à terme car il n'y aurait personne pour leur fournir la valeur de retour.
Marius Danila
4
Je pense que je vois ce que vous entendez par utilisations du monde réel: j'ai mis à jour ma réponse pour vous donner un exemple.
Marius Danila
2
@Marius: Compte tenu de l'exemple du monde réel donné, et si makeHTTPCall était implémenté comme ceci: def makeHTTPCall(request: Request): Future[Response] = { Future { registerOnCompleteCallback(buffer => { val response = makeResponse(buffer) response }) } }
puneetk
1
@puneetk alors vous aurez l'avenir, qui se termine juste après avoir registerOnCompleteCallback()terminé. De plus, il ne revient pas Future[Response]. Il revient à la Future[registerOnCompleteCallback() return type]place.
Evgeny Veretennikov