BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Actualités JEP 428  : Structured Concurrency Pour Simplifier La Programmation Java Multithread

JEP 428  : Structured Concurrency Pour Simplifier La Programmation Java Multithread

La JEP 428, Structured Concurrency (Incubator), a été promue du statut Proposed to Target à Targeted pour le JDK 19. Sous l'égide du Projet Loom, cette JEP propose de simplifier la programmation multithread en introduisant une bibliothèque pour traiter plusieurs tâches s'exécutant sur différents threads comme un fonctionnement atomique. En conséquence, il rationalisera la gestion et l'annulation des erreurs, améliorera la fiabilité et améliorera l'observabilité. Il s'agit toujours d'une API en incubation.

Cela permet aux développeurs d'organiser leur code concurrent à l'aide de la classe StructuredTaskScope. Elle traitera une famille de sous-tâches comme une unité. Les sous-tâches seront créées sur leurs propres threads en les bifurquant individuellement, mais ensuite jointes en tant qu'unité et éventuellement annulées en tant qu'unité ; leurs exceptions ou résultats positifs seront agrégés et gérés par la tâche parent. Voyons un exemple :

Response handle() throws ExecutionException, InterruptedException {
   try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
       Future<String> user = scope.fork(() -> findUser());
       Future<Integer> order = scope.fork(() -> fetchOrder());

       scope.join();          // Join both forks
       scope.throwIfFailed(); // ... and propagate errors

       // Here, both forks have succeeded, so compose their results
       return new Response(user.resultNow(), order.resultNow());
   }
}

La méthode handle() ci-dessus représente une tâche dans une application serveur. Elle gère une demande entrante en créant deux sous-tâches. Comme ExecutorService.submit(), StructuredTaskScope.fork() prend un Callable et renvoie un Future. Contrairement à ExecutorService, la Future renvoyée n'est pas joint via Future.get(). Cette API s'exécute au-dessus de la JEP 425, Virtual Threads (Preview), également ciblés pour JDK 19.

Les exemples ci-dessus utilisent l'API StructuredTaskScope, donc pour les exécuter sur le JDK 19, un développeur doit ajouter le module jdk.incubator.concurrent, ainsi que l'activation des fonctionnalités de prévisualisation pour utiliser les threads virtuels :

Compilez le code ci-dessus comme indiqué dans la commande suivante :

javac --release 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java

Le même flag est également requis pour exécuter le programme :

java --enable-preview --add-modules jdk.incubator.concurrent Main

Cependant, on peut l'exécuter directement en utilisant le lanceur de code source. Dans ce cas, la ligne de commande serait :

java --source 19 --enable-preview --add-modules jdk.incubator.concurrent Main.java

L'utilisation de jshell est également possible, mais nécessite également l'activation de la fonction d'aperçu :

jshell --enable-preview --add-modules jdk.incubator.concurrent

Les avantages apportés par la concurrence structurée sont nombreux. Elle crée une relation enfant-parent entre la méthode de l'invocateur et ses sous-tâches. Par exemple, dans l'exemple ci-dessus, la tâche handle() est un parent et ses sous-tâches, findUser() et fetchOrder(), sont des enfants. En conséquence, tout le bloc de code devient atomique. Il garantit l'observabilité en démontrant la hiérarchie des tâches dans le thread dump. Il permet également le court-circuit dans la gestion des erreurs. Si l'une des sous-tâches échoue, les autres tâches seront annulées si elles ne sont pas terminées. Si le thread de la tâche parent est interrompu avant ou pendant l'appel à join(), les deux forks seront automatiquement annulés à la sortie du scope. Cela clarifie la structure du code concurrent, et le développeur peut désormais raisonner et suivre le code en le lisant comme s'il s'exécutait dans un environnement mono-thread.

Au début de la programmation, le flux d'un programme était contrôlé par l'utilisation omniprésente de l'instruction GOTO, et il en résultait un code confus et spaghetti difficile à lire et déboguer. Au fur et à mesure que le paradigme de la programmation mûrissait, la communauté des programmeurs comprenait que l'instruction GOTO était mauvaise. En 1969, Donald Knuth, un informaticien largement connu pour le livre The Art of Computer Programming a défendu que les programmes peut être écrit efficacement sans GOTO. Plus tard, la programmation structurée est apparue pour résoudre toutes ces lacunes. Considérez l'exemple suivant :

Response handle() throws IOException {
   String theUser = findUser();
   int theOrder = fetchOrder();
   return new Response(theUser, theOrder);
}

Le code ci-dessus est un exemple de code structuré. Dans un environnement monothread, il est exécuté séquentiellement lorsque la méthode handle() est appelée. La méthode fetchOrder() ne démarre pas avant la méthode findUser(). Si la méthode findUser() échoue, l'invocation de la méthode suivante ne démarrera pas du tout et la méthode handle() échoue implicitement, ce qui garantit à son tour que l'opération atomique réussit ou échoue. Cela nous donne une relation parent-enfant entre la méthode handle() et ses appels de méthode enfant, qui suit la propagation des erreurs et nous donne une pile d'appels au moment de l'exécution.

Cependant, cette approche et ce raisonnement ne fonctionnent pas avec notre modèle de programmation de thread actuel. Par exemple, si nous voulons écrire le code ci-dessus avec un ExecutorService, le code devient le suivant :

Response handle() throws ExecutionException, InterruptedException {
   Future<String>  user  = executorService.submit(() -> findUser());
   Future<Integer> order = executorService.submit(() -> fetchOrder());
   String theUser  = user.get();   // Join findUser
   int theOrder = order.get();  // Join fetchOrder
   return new Response(theUser, theOrder);
}

Les sous-tâches de l'ExecutorService s'exécutent indépendamment, elles peuvent donc réussir ou échouer indépendamment. L'interruption ne se propage pas aux sous-tâches même si le parent est interrompu et crée ainsi un scénario de fuite. Il perd la relation parentale. Cela complique également le débogage car les tâches parent et enfant apparaissent sur la pile d'appels de threads non liés dans le thread-dump. Bien que le code puisse sembler logiquement structuré, il reste dans l'esprit du développeur plutôt qu'en cours d'exécution ; ainsi, le code concurrent devient non structuré.

En observant tous ces problèmes avec le code concurrent non structuré, le terme "Structured Concurrency" a été inventé par Martin Sústrik dans son article de blog puis popularisé par Nathaniel J. Smith dans son article Notes on structured concurrency. À propos de la concurrence structurée, Ron Pressler, membre consultant de l'équipe technique d'Oracle et chef de projet du projet Loom, dans un podcast InfoQ, dit :

Structuré signifie que si vous générez quelque chose, vous devez l'attendre et le rejoindre. Et le mot structure ici est similaire à son utilisation dans la programmation structurée. Et l'idée est que la structure en blocs de votre code reflète le comportement d'exécution du programme. Ainsi, tout comme la programmation structurée vous donne cela pour le flux de contrôle séquentiel, la concurrence structurée fait la même chose pour la concurrence.

Les développeurs intéressés par une plongée approfondie dans la concurrence structurée et l'apprentissage de son historique peuvent écouter le Podcast InfoQ, un Session YouTube par Ron Pressler et les articles Java.

 

Au sujet de l’Auteur

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT