Programmation parallèle et asynchrone en Java 8

Le code parallèle, qui est du code qui s’exécute sur plus d’un thread, était autrefois le cauchemar de nombreux développeurs expérimentés, mais Java 8 a apporté de nombreux changements qui devraient rendre cette astuce améliorant les performances beaucoup plus gérable.

Flux parallèles

Avant Java 8, il y avait une grande différence entre le code parallèle (ou concurrent) et le code séquentiel. Il était également très difficile de déboguer du code non séquentiel. Le simple fait de définir un point d’arrêt et de parcourir le flux comme vous le feriez normalement supprimerait l’aspect parallèle, ce qui est un problème si c’est ce qui cause le bogue.

Heureusement, Java 8 nous a donné des flux, la meilleure chose pour les développeurs Java depuis le bean. Si vous ne savez pas ce qu’ils sont, l’API Stream permet de gérer des séquences d’éléments dans une matière fonctionnelle. (Consultez notre comparaison entre les flux et LINQ de .NET ici.) L’un des avantages des flux est que la structure du code reste la même: qu’il soit séquentiel ou simultané, il reste tout aussi lisible.

Pour que votre code fonctionne en parallèle, il vous suffit d’utiliser .parallelStream() au lieu de .stream(), (ou stream.parallel(), si vous n’êtes pas le créateur du flux).

Mais ce n’est pas parce que c’est facile que le code parallèle est toujours le meilleur choix. Vous devez toujours vous demander s’il est logique d’utiliser la concurrence pour votre morceau de code. Le facteur le plus important dans cette décision sera la vitesse: n’utilisez la concurrence que si cela rend votre code plus rapide que son homologue séquentiel.

La question de vitesse

Le code parallèle tire son avantage de vitesse de l’utilisation de plusieurs threads au lieu du seul que le code séquentiel utilise. Décider du nombre de threads à créer peut être une question délicate car plus de threads n’entraînent pas toujours un code plus rapide: si vous utilisez trop de threads, les performances de votre code peuvent en fait baisser.

Il existe quelques règles qui vous indiqueront le nombre de threads à choisir. Cela dépend principalement du type d’opération que vous souhaitez effectuer et du nombre de cœurs disponibles.

Les opérations intensives en calcul doivent utiliser un nombre de threads inférieur ou égal au nombre de cœurs, tandis que les opérations intensives en E/ S comme la copie de fichiers n’ont aucune utilité pour le processeur et peuvent donc utiliser un nombre plus élevé de threads. Le code ne sait pas quel cas est applicable à moins que vous ne lui disiez quoi faire. Sinon, il aura par défaut un nombre de threads égal au nombre de cœurs.

Il existe deux cas principaux où il peut être utile d’exécuter votre code en parallèle au lieu de séquentiel: les tâches chronophages et les tâches exécutées sur de grandes collections. Java 8 a apporté une nouvelle façon de gérer ces grandes collections, à savoir avec les flux. Les flux ont une efficacité intégrée par paresse: ils utilisent une évaluation paresseuse qui économise des ressources en ne faisant pas plus que nécessaire. Ce n’est pas la même chose que le parallélisme, qui ne se soucie pas des ressources tant que cela va plus vite. Donc, pour les grandes collections, vous n’avez probablement pas besoin de parallélisme classique.

Aller Asynchrone

Leçons de JavaScript

Il est rare qu’un développeur Java puisse dire qu’il a appris quelque chose en regardant JavaScript, mais en ce qui concerne la programmation asynchrone, JavaScript a en fait eu raison en premier. En tant que langage fondamentalement asynchrone, JavaScript a beaucoup d’expérience avec la douleur qu’il peut être lorsqu’il est mal implémenté. Cela a commencé par des rappels et a ensuite été remplacé par des promesses. Un avantage important de promises est qu’il dispose de deux « canaux »: un pour les données et un pour les erreurs. Une promesse JavaScript pourrait ressembler à ceci:

func.then(f1).catch(e1).then(f2).catch(e2);

Ainsi, lorsque la fonction d’origine a un résultat réussi, f1 est appelé, mais si une erreur a été levée, e1 sera appelé. Cela peut le ramener à la piste réussie (f2) ou entraîner une autre erreur (e2). Vous pouvez passer de la piste de données à la piste d’erreur et inversement.

La version Java de JavaScript promises s’appelle CompletableFuture.

CompletableFuture

CompletableFuture implémente à la fois l’interface Future et l’interface CompletionStage. Future existait déjà avant Java8, mais ce n’était pas très convivial pour les développeurs en soi. Vous ne pouviez obtenir le résultat du calcul asynchrone qu’en utilisant la méthode .get(), qui bloquait le reste (rendant la partie asynchrone assez inutile la plupart du temps) et vous deviez implémenter chaque scénario possible manuellement. L’ajout de l’interface CompletionStage a été la percée qui a rendu la programmation asynchrone en Java réalisable.

CompletionStage est une promesse, à savoir la promesse que le calcul sera finalement fait. Il contient un tas de méthodes qui vous permettent d’attacher des rappels qui seront exécutés à cette fin. Maintenant, nous pouvons gérer le résultat sans bloquer.

Il existe deux méthodes principales qui vous permettent de démarrer la partie asynchrone de votre code: supplyAsync si vous voulez faire quelque chose avec le résultat de la méthode, et runAsync si vous ne le faites pas.

CompletableFuture.runAsync(() → System.out.println("Run async in completable future " + Thread.currentThread()));CompletableFuture.supplyAsync(() → 5);

Callbacks

Maintenant, vous pouvez ajouter ces callbacks pour gérer le résultat de votre supplyAsync.

CompletableFuture.supplyAsync(() → 5).thenApply(i → i * 3).thenAccept(i → System.out.println("The result is " + i).thenRun(() → System.out.println("Finished."));

.thenApply est similaire à la fonction .map pour les flux : elle effectue une transformation. Dans l’exemple ci-dessus, il prend le résultat (5) et le multiplie par 3. Il fera ensuite passer ce résultat (15) plus loin dans le tuyau.

.thenAccept effectue une méthode sur le résultat sans le transformer. Il ne retournera pas non plus de résultat. Ici, il affichera « Le résultat est de 15 » sur la console. Elle peut être comparée à la méthode .foreach pour les flux.

.thenRun n’utilise pas le résultat de l’opération asynchrone et ne renvoie rien non plus, il attend simplement d’appeler son Runnable jusqu’à ce que l’étape précédente soit terminée.

Asynchronisation de votre Asynchrone

Toutes les méthodes de rappel ci-dessus sont également disponibles en version asynchrone: thenRunAsync, thenApplyAsync, etc. Ces versions peuvent fonctionner sur leur propre thread et elles vous donnent un contrôle supplémentaire car vous pouvez lui dire lequel ForkJoinPool utiliser.

Si vous n’utilisez pas la version asynchrone, les rappels seront tous exécutés sur le même thread.

Lorsque les choses tournent mal

Lorsque quelque chose tourne mal, la méthode exceptionally est utilisée pour gérer l’exception. Vous pouvez lui donner une méthode qui renvoie une valeur pour revenir sur la piste de données ou lancer une (nouvelle) exception.

….exceptionally(ex → new Foo()).thenAccept(this::bar);

Combiner et composer

Vous pouvez enchaîner plusieurs CompletableFutures en utilisant la méthode thenCompose. Sans cela, le résultat serait imbriqué CompletableFutures. Cela fait thenCompose et thenApply comme flatMap et map pour les flux.

CompletableFuture.supplyAsync(() -> "Hello").thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "World"));

Si vous souhaitez combiner le résultat de deux CompletableFutures, vous aurez besoin d’une méthode commodément appelée thenCombine.

future.thenCombine(future2, Integer::sum).thenAccept(value → System.out.println(value));

Comme vous pouvez le voir dans l’exemple ci-dessus, le résultat du rappel dans thenCombine peut être géré comme un CompletableFuture normal avec toutes vos méthodes CompletionStage préférées.

Conclusion

La programmation parallèle ne doit plus être un obstacle insurmontable dans la chasse au code plus rapide. Java 8 rend le processus aussi simple que possible, de sorte que tout morceau de code qui pourrait en bénéficier peut être tiré, botté et hurlé sur tous les threads, dans le futur multicœur qui n’est, en fait, que le présent. Par ce que je veux dire: c’est facile à faire, alors essayez-le et voyez ses avantages par vous-même.



+