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.
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.
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.
CompletableFuture
est asynchrone et peut être non bloquantCompletableFuture
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.