Avec l'avènement des processeurs multicœurs ces dernières années, la programmation parallèle est le moyen de tirer pleinement parti des nouveaux chevaux de travail de traitement. Programmation parallèle fait référence à l'exécution simultanée de processus en raison de la disponibilité de plusieurs cœurs de traitement. Ceci, en substance, conduit à une augmentation considérable des performances et de l'efficacité des programmes, contrairement à l'exécution linéaire à un seul cœur ou même au multithreading. Le framework Fork/Join fait partie de l'API de concurrence Java. Ce cadre permet aux programmeurs de paralléliser les algorithmes. Cet article explore le concept de programmation parallèle à l'aide du framework Fork/Join disponible en Java.
Un aperçu
La programmation parallèle a une connotation beaucoup plus large et est sans aucun doute un vaste domaine à développer en quelques lignes. Le nœud du problème est assez simple, mais opérationnellement beaucoup plus difficile à réaliser. En termes simples, la programmation parallèle signifie écrire des programmes qui utilisent plus d'un processeur pour accomplir une tâche, c'est tout ! Devinez quoi; cela semble familier, n'est-ce pas? Cela rime presque avec l'idée de multithreading. Mais, notez qu'il existe des distinctions importantes entre eux. En surface, ils sont identiques, mais le courant sous-jacent est absolument différent. En fait, le multithreading a été introduit pour fournir une sorte d'illusion de traitement parallèle sans véritable exécution parallèle. Ce que le multithreading fait vraiment, c'est qu'il vole le temps d'inactivité du processeur et l'utilise à son avantage.
En bref, le multithreading est une collection d'unités logiques discrètes de tâches qui s'exécutent pour récupérer leur part de temps CPU tandis qu'un autre thread peut temporairement attendre, par exemple, une entrée de l'utilisateur. Le temps CPU inactif est partagé de manière optimale entre les threads concurrents. S'il n'y a qu'un seul processeur, il est en temps partagé. S'il y a plusieurs cœurs de processeur, ils sont également tous partagés en temps. Par conséquent, un programme multithread optimal réduit les performances du processeur par le mécanisme intelligent du partage de temps. Essentiellement, il s'agit toujours d'un thread utilisant un processeur pendant qu'un autre thread attend. Cela se produit de manière subtile pour que l'utilisateur ait une idée du traitement parallèle où, en réalité, le traitement se déroule en succession rapide. Le plus grand avantage du multithreading est qu'il s'agit d'une technique permettant de tirer le meilleur parti des ressources de traitement. Maintenant, cette idée est très utile et peut être utilisée dans n'importe quel ensemble d'environnements, qu'il ait un seul processeur ou plusieurs processeurs. L'idée est la même.
La programmation parallèle, en revanche, signifie qu'il existe plusieurs processeurs dédiés qui sont exploités en parallèle par le programmeur. Ce type de programmation est optimisé pour un environnement CPU multicœur. La plupart des machines d'aujourd'hui utilisent des processeurs multicœurs. Par conséquent, la programmation parallèle est tout à fait pertinente de nos jours. Même la machine la moins chère est montée avec des processeurs multicœurs. Regardez les appareils portatifs ; même ils sont multicœurs. Bien que tout semble parfait avec les processeurs multicœurs, voici également un autre aspect de l'histoire. Plus de cœurs de processeur signifient-ils un calcul plus rapide ou efficace ? Pas toujours! La philosophie gourmande du "plus on est de fous" ne s'applique pas à l'informatique, ni à la vie. Mais ils sont là, unignorable-dual, quad, octa, et ainsi de suite. Ils sont là principalement parce que nous les voulons et non parce que nous en avons besoin, du moins dans la plupart des cas. En réalité, il est relativement difficile de maintenir ne serait-ce qu'un seul processeur occupé dans l'informatique quotidienne. Cependant, les multicœurs ont leurs utilisations dans des circonstances particulières, telles que les serveurs, les jeux, etc., ou la résolution de problèmes importants. Le problème d'avoir plusieurs processeurs est qu'il nécessite une mémoire qui doit correspondre à la vitesse avec la puissance de traitement, ainsi que des canaux de données ultra-rapides et d'autres accessoires. En bref, plusieurs cœurs de processeur dans l'informatique quotidienne offrent une amélioration des performances qui ne peut pas dépasser la quantité de ressources nécessaires pour l'utiliser. Par conséquent, nous obtenons une machine coûteuse sous-utilisée, peut-être destinée uniquement à être présentée.
Programmation parallèle
Contrairement au multithreading, où chaque tâche est une unité logique discrète d'une tâche plus grande, les tâches de programmation parallèles sont indépendantes et leur ordre d'exécution n'a pas d'importance. Les tâches sont définies en fonction de la fonction qu'elles remplissent ou des données utilisées dans le traitement ; c'est ce qu'on appelle le parallélisme fonctionnel ou parallélisme des données , respectivement. Dans le parallélisme fonctionnel, chaque processeur travaille sur sa section du problème alors que dans le parallélisme de données, le processeur travaille sur sa section des données. La programmation parallèle convient à une base de problèmes plus large qui ne s'intègre pas dans une seule architecture de processeur, ou il se peut que le problème soit si important qu'il ne puisse pas être résolu dans un délai raisonnable. Par conséquent, les tâches, lorsqu'elles sont réparties entre les processeurs, peuvent obtenir le résultat relativement rapidement.
Le framework fork/join
Le framework Fork/Join est défini dans java.util.concurrent emballer. Il comprend plusieurs classes et interfaces qui prennent en charge la programmation parallèle. Ce qu'il fait principalement, c'est qu'il simplifie le processus de création de plusieurs threads, leurs utilisations et automatise le mécanisme d'allocation de processus entre plusieurs processeurs. La différence notable entre le multithreading et la programmation parallèle avec ce framework est très similaire à ce que nous avons mentionné précédemment. Ici, la partie traitement est optimisée pour utiliser plusieurs processeurs contrairement au multithreading, où le temps d'inactivité du processeur unique est optimisé sur la base du temps partagé. L'avantage supplémentaire de ce framework est d'utiliser le multithreading dans un environnement d'exécution parallèle. Pas de mal là-bas.
Il existe quatre classes de base dans ce framework :
- ForkJoinTask
: Il s'agit d'une classe abstraite qui définit une tâche. Typiquement, une tâche est créée à l'aide de fork() méthode définie dans cette classe. Cette tâche est presque similaire à un thread normal créé avec le Thread classe, mais il est plus léger qu'elle. Le mécanisme qu'il applique est qu'il permet la gestion d'un grand nombre de tâches à l'aide d'un petit nombre de threads réels qui rejoignent le ForkJoinPool . La fourchette() La méthode permet l'exécution asynchrone de la tâche d'appel. Le join() La méthode permet d'attendre que la tâche sur laquelle elle est appelée soit finalement terminée. Il existe une autre méthode, appelée invoke() , qui combine le fork et rejoignez opérations en un seul appel. - ForkJoinPool : Cette classe fournit un pool commun pour gérer l'exécution de ForkJoinTask Tâches. Il fournit essentiellement le point d'entrée pour les soumissions de non-ForkJoinTask clients, ainsi que les opérations de gestion et de suivi.
- Action récursive : C'est aussi une extension abstraite de la ForkJoinTask classe. Généralement, nous étendons cette classe pour créer une tâche qui ne renvoie pas de résultat ou qui a un void type de retour. Le compute() La méthode définie dans cette classe est remplacée pour inclure le code de calcul de la tâche.
- Tâche récursive
: Ceci est une autre extension abstraite de la ForkJoinTask classe. Nous étendons cette classe pour créer une tâche qui renvoie un résultat. Et, similaire à ResursiveAction, il inclut également un protected abstract compute() méthode. Cette méthode est remplacée pour inclure la partie calcul de la tâche.
La stratégie du cadre Fork/Join
Ce framework utilise une méthode récursive diviser pour mieux régner stratégie de mise en œuvre du traitement parallèle. Il divise essentiellement une tâche en sous-tâches plus petites ; ensuite, chaque sous-tâche est ensuite divisée en sous-sous-tâches. Ce processus est appliqué de manière récursive sur chaque tâche jusqu'à ce qu'elle soit suffisamment petite pour être traitée séquentiellement. Supposons que nous devions incrémenter les valeurs d'un tableau de N Nombres. C'est la tâche. Maintenant, nous pouvons diviser le tableau par deux en créant deux sous-tâches. Divisez à nouveau chacune d'elles en deux autres sous-tâches, et ainsi de suite. De cette façon, nous pouvons appliquer un diviser pour mieux régner stratégie de manière récursive jusqu'à ce que les tâches soient isolées dans un problème unitaire. Ce problème unitaire peut alors être exécuté en parallèle par les processeurs multicœurs disponibles. Dans un environnement non parallèle, nous devions parcourir l'ensemble du réseau et effectuer le traitement en séquence. Il s'agit clairement d'une approche inefficace compte tenu du traitement parallèle. Mais la vraie question est de savoir si chaque problème peut être divisé et conquis ? Définitivement non! Mais, il y a des problèmes qui impliquent souvent une sorte de tableau, de collecte, de regroupement de données qui convient particulièrement à cette approche. Soit dit en passant, il existe des problèmes qui peuvent ne pas utiliser la collecte de données mais qui peuvent être optimisés pour utiliser la stratégie de programmation parallèle. Le type de problèmes de calcul qui convient au traitement parallèle ou à la discussion sur l'algorithme parallèle sort du cadre de cet article. Voyons un exemple rapide sur l'application du framework Fork/Join.
Un exemple rapide
Ceci est un exemple très simple pour vous donner une idée sur la façon d'implémenter le parallélisme en Java avec le Fork/Join Framework.
package org.mano.example; import java.util.concurrent.RecursiveAction; public class CustomRecursiveAction extends RecursiveAction { final int THRESHOLD = 2; double [] numbers; int indexStart, indexLast; CustomRecursiveAction(double [] n, int s, int l) { numbers = n; indexStart = s; indexLast = l; } @Override protected void compute() { if ((indexLast - indexStart) > THRESHOLD) for (int i = indexStart; i < indexLast; i++) numbers [i] = numbers [i] + Math.random(); else invokeAll (new CustomRecursiveAction(numbers, indexStart, (indexStart - indexLast) / 2), new CustomRecursiveAction(numbers, (indexStart - indexLast) / 2, indexLast)); } } package org.mano.example; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { final int SIZE = 10; ForkJoinPool pool = new ForkJoinPool(); double na[] = new double [SIZE]; System.out.println("initialized random values :"); for (int i = 0; i < na.length; i++) { na[i] = (double) i + Math.random(); System.out.format("%.4f ", na[i]); } System.out.println(); CustomRecursiveAction task = new CustomRecursiveAction(na, 0, na.length); pool.invoke(task); System.out.println("Changed values :"); for (inti = 0; i < 10; i++) System.out.format("%.4f ", na[i]); System.out.println(); } }
Conclusion
Il s'agit d'une description succincte de la programmation parallèle et de la manière dont elle est prise en charge en Java. C'est un fait bien établi qu'avoir N les cœurs ne vont pas tout faire N fois plus vite. Seule une partie des applications Java utilise effectivement cette fonctionnalité. Le code de programmation parallèle est un cadre difficile. De plus, des programmes parallèles efficaces doivent prendre en compte des problèmes tels que l'équilibrage de charge, la communication entre des tâches parallèles, etc. Certains algorithmes conviennent mieux à l'exécution parallèle, mais beaucoup ne le font pas. En tout cas, l'API Java ne manque pas de son support. Nous pouvons toujours bricoler les API pour savoir ce qui convient le mieux. Bon codage 🙂