web-dev-qa-db-fra.com

Mono vs CompletableFuture

CompletableFuture exécute une tâche sur un thread séparé (utilise un pool de threads) et fournit une fonction de rappel. Disons que j'ai un appel API dans un CompletableFuture. Est-ce un blocage d'appels API? Le thread serait-il bloqué jusqu'à ce qu'il n'obtienne pas de réponse de l'API? (Je sais que le thread principal/thread Tomcat ne sera pas bloquant, mais qu'en est-il du thread sur lequel la tâche CompletableFuture s'exécute?)

Mono est complètement non bloquant, pour autant que je sache.

Veuillez éclairer cela et corrigez-moi si je me trompe.

15
XYZ

CompletableFuture est Async. Mais est-ce non bloquant?

Un qui est vrai à propos de CompletableFuture est qu'il est vraiment asynchrone, il vous permet d'exécuter votre tâche de manière asynchrone à partir du thread d'appelant et l'API telle que thenXXX vous permet de traiter le résultat lorsqu'il devient disponible. En revanche, CompletableFuture n'est pas toujours non bloquant. Par exemple, lorsque vous exécutez le code suivant, il sera exécuté de manière asynchrone sur le ForkJoinPool par défaut:

CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    }
    catch (InterruptedException e) {

    }

    return 1;
});

Il est clair que le Thread dans ForkJoinPool qui exécute la tâche sera finalement bloqué, ce qui signifie que nous ne pouvons pas garantir que l'appel sera non bloquant.

D'autre part, CompletableFuture expose l'API qui vous permet de la rendre vraiment non bloquante.

Par exemple, vous pouvez toujours effectuer les opérations suivantes:

public CompletableFuture myNonBlockingHttpCall(Object someData) {
    var uncompletedFuture = new CompletableFuture(); // creates uncompleted future

    myAsyncHttpClient.execute(someData, (result, exception -> {
        if(exception != null) {
            uncompletedFuture.completeExceptionally(exception);
            return;
        }
        uncompletedFuture.complete(result);
    })

    return uncompletedFuture;
}

Comme vous pouvez le voir, l'API de CompletableFuture future vous fournit les méthodes complete et completeExceptionally qui complètent votre exécution chaque fois que cela est nécessaire sans bloquer aucun thread.

Mono vs CompletableFuture

Dans la section précédente, nous avons eu un aperçu du comportement des FC, mais quelle est la principale différence entre CompletableFuture et Mono?

Il convient de mentionner que nous pouvons également bloquer Mono. Personne ne nous empêche d'écrire ce qui suit:

Mono.fromCallable(() -> {
    try {
        Thread.sleep(1000);
    }
    catch (InterruptedException e) {

    }

    return 1;
})

Bien sûr, une fois que nous nous abonnerons à l'avenir, le fil d'appel sera bloqué. Mais nous pouvons toujours contourner cela en fournissant un opérateur subscribeOn supplémentaire. Néanmoins, l'API plus large de Mono n'est pas la caractéristique clé.

Afin de comprendre la principale différence entre CompletableFuture et Mono, revenons à l'implémentation de la méthode myNonBlockingHttpCall mentionnée précédemment.

public CompletableFuture myUpperLevelBusinessLogic() {
    var future = myNonBlockingHttpCall();

    // ... some code

    if (something) {
       // oh we don't really need anything, let's just throw an exception
       var errorFuture = new CompletableFuture();
       errorFuture.completeExceptionally(new RuntimeException());

       return errorFuture;
    }

   return future;
}

Dans le cas de CompletableFuture, une fois la méthode appelée, elle exécutera avec impatience l'appel HTTP vers un autre service/ressource. Même si nous n'aurons pas vraiment besoin du résultat de l'exécution après avoir vérifié certaines conditions de pré/post, il démarre l'exécution et des ressources CPU/DB-Connections/What-Ever-Machine supplémentaires seront allouées pour ce travail.

En revanche, le type Mono est paresseux par définition:

public Mono myNonBlockingHttpCallWithMono(Object someData) {
    return Mono.create(sink -> {
            myAsyncHttpClient.execute(someData, (result, exception -> {
                if(exception != null) {
                    sink.error(exception);
                    return;
                }
                sink.success(result);
            })
    });
} 

public Mono myUpperLevelBusinessLogic() {
    var mono = myNonBlockingHttpCallWithMono();

    // ... some code

    if (something) {
       // oh we don't really need anything, let's just throw an exception

       return Mono.error(new RuntimeException());
    }

   return mono;
}

Dans ce cas, rien ne se passera tant que le mono final ne sera pas abonné. Ainsi, seulement lorsque Mono retourné par la méthode myNonBlockingHttpCallWithMono sera abonné, la logique fournie à Mono.create(Consumer) sera exécutée.

Et nous pouvons aller encore plus loin. Nous pouvons rendre notre exécution beaucoup plus paresseuse. Comme vous le savez peut-être, Mono étend Publisher à partir de la spécification Reactive Streams. La fonction hurlante de Reactive Streams est la prise en charge de la contre-pression. Ainsi, en utilisant l'API Mono, nous ne pouvons exécuter que lorsque les données sont vraiment nécessaires, et notre abonné est prêt à les consommer:

Mono.create(sink -> {
    AtomicBoolean once = new AtomicBoolean();
    sink.onRequest(__ -> {
        if(!once.get() && once.compareAndSet(false, true) {
            myAsyncHttpClient.execute(someData, (result, exception -> {
                if(exception != null) {
                    sink.error(exception);
                    return;
                }
                sink.success(result);
            });
        }
    });
});

Dans cet exemple, nous exécutons les données uniquement lorsque l'abonné a appelé Subscription#request, Ce faisant, il a déclaré qu'il était prêt à recevoir des données.

Résumé

  • CompletableFuture est asynchrone et peut être non bloquant
  • CompletableFuture est impatient. Vous ne pouvez pas reporter l'exécution. Mais vous pouvez les annuler (ce qui est mieux que rien)
  • Mono est asynchrone/non bloquant et peut facilement exécuter n'importe quel appel sur différents Thread en composant le Mono principal avec différents opérateurs.
  • Mono est vraiment paresseux et permet de reporter le démarrage de l'exécution par la présence de l'abonné et sa disponibilité à consommer des données.
21
Oleh Dokuka