Guide du framework Fork/Join en Java

Guide du framework Fork / Join en Java

1. Vue d'ensemble

Le framework fork / join a été présenté dans Java 7. Il fournit des outils pour accélérer le traitement parallèle en essayant d'utiliser tous les cœurs de processeur disponibles - ce qui est accomplithrough a divide and conquer approach.

En pratique, cela signifie quethe framework first “forks” divise récursivement la tâche en sous-tâches indépendantes plus petites jusqu'à ce qu'elles soient suffisamment simples pour être exécutées de manière asynchrone.

Après cela,the “join” part begins, dans lequel les résultats de toutes les sous-tâches sont joints de manière récursive en un seul résultat, ou dans le cas d'une tâche qui retourne void, le programme attend simplement que chaque sous-tâche soit exécutée.

Pour fournir une exécution parallèle efficace, le framework fork / join utilise un pool de threads appeléForkJoinPool, qui gère les threads de travail de typeForkJoinWorkerThread.

2. ForkJoinPool

LeForkJoinPool est le cœur du framework. Il s'agit d'une implémentation desExecutorService qui gère les threads de travail et nous fournit des outils pour obtenir des informations sur l'état et les performances du pool de threads.

Les threads de travail ne peuvent exécuter qu’une seule tâche à la fois, mais lesForkJoinPool ne créent pas de thread distinct pour chaque sous-tâche. Au lieu de cela, chaque thread du pool a sa propre file d'attente double (oudeque, prononcédeck) qui stocke les tâches.

Cette architecture est vitale pour équilibrer la charge de travail du thread à l’aide deswork-stealing algorithm.

2.1. Algorithme de vol de travail

En termes simples, les threads libres essaient de «voler» le travail de plusieurs threads occupés.

Par défaut, un thread de travail obtient les tâches de la tête de son propre deque. Lorsqu'il est vide, le thread prend une tâche dans la queue du déque d'un autre thread occupé ou dans la file d'attente des entrées globales, car c'est là que se trouvent les plus gros morceaux de travail.

Cette approche minimise la possibilité que les threads se font concurrence pour les tâches. Cela réduit également le nombre de fois que le fil devra chercher du travail, car il fonctionne en premier sur les plus gros morceaux de travail disponibles.

2.2. ForkJoinPool Instanciation

Dans Java 8, le moyen le plus pratique d'accéder à l'instance duForkJoinPool est d'utiliser sa méthode statiquecommonPool(). Comme son nom l'indique, cela fournira une référence au pool commun, qui est un pool de threads par défaut pour chaqueForkJoinTask.

SelonOracle’s documentation, l'utilisation du pool commun prédéfini réduit la consommation de ressources, car cela décourage la création d'un pool de threads séparé par tâche.

ForkJoinPool commonPool = ForkJoinPool.commonPool();

Le même comportement peut être obtenu dans Java 7 en créant unForkJoinPool et en l'affectant à un champpublic static d'une classe utilitaire:

public static ForkJoinPool forkJoinPool = new ForkJoinPool(2);

Maintenant, il est facilement accessible:

ForkJoinPool forkJoinPool = PoolUtil.forkJoinPool;

Avec les constructeursForkJoinPool’s, il est possible de créer un pool de threads personnalisé avec un niveau spécifique de parallélisme, de fabrique de threads et de gestionnaire d'exceptions. Dans l'exemple ci-dessus, la piscine a un niveau de parallélisme de 2. Cela signifie que le pool utilisera 2 cœurs de processeur.

3. ForkJoinTask<V>

ForkJoinTask est le type de base des tâches exécutées à l'intérieur deForkJoinPool. En pratique, l'une de ses deux sous-classes devrait être étendue: lesRecursiveAction pour les tâchesvoid et lesRecursiveTask<V> pour les tâches qui renvoient une valeur. _ They both have an abstract method _compute() dans lequel la logique de la tâche est définie.

3.1. RecursiveAction – An Example

Dans l'exemple ci-dessous, l'unité de travail à traiter est représentée par unString appeléworkload. À des fins de démonstration, la tâche est absurde: il met simplement en majuscule ses entrées et les enregistre.

Pour démontrer le comportement de fourche du framework, la méthodethe example splits the task if workload.length() is larger than a specified threshold_ using the _createSubtask().

La chaîne est divisée de manière récursive en sous-chaînes, créant des instancesCustomRecursiveTask basées sur ces sous-chaînes.

En conséquence, la méthode renvoie unList<CustomRecursiveAction>.

La liste est soumise auxForkJoinPool en utilisant la méthodeinvokeAll():

public class CustomRecursiveAction extends RecursiveAction {

    private String workload = "";
    private static final int THRESHOLD = 4;

    private static Logger logger =
      Logger.getAnonymousLogger();

    public CustomRecursiveAction(String workload) {
        this.workload = workload;
    }

    @Override
    protected void compute() {
        if (workload.length() > THRESHOLD) {
            ForkJoinTask.invokeAll(createSubtasks());
        } else {
           processing(workload);
        }
    }

    private List createSubtasks() {
        List subtasks = new ArrayList<>();

        String partOne = workload.substring(0, workload.length() / 2);
        String partTwo = workload.substring(workload.length() / 2, workload.length());

        subtasks.add(new CustomRecursiveAction(partOne));
        subtasks.add(new CustomRecursiveAction(partTwo));

        return subtasks;
    }

    private void processing(String work) {
        String result = work.toUpperCase();
        logger.info("This result - (" + result + ") - was processed by "
          + Thread.currentThread().getName());
    }
}

Ce modèle peut être utilisé pour développer vos propresRecursiveAction classes. Pour ce faire, créez un objet qui représente la quantité totale de travail, choisissez un seuil approprié, définissez une méthode pour diviser le travail et définissez une méthode pour faire le travail.

3.2. RecursiveTask<V>

Pour les tâches qui renvoient une valeur, la logique ici est similaire, sauf que le résultat de chaque sous-tâche est uni en un seul résultat:

public class CustomRecursiveTask extends RecursiveTask {
    private int[] arr;

    private static final int THRESHOLD = 20;

    public CustomRecursiveTask(int[] arr) {
        this.arr = arr;
    }

    @Override
    protected Integer compute() {
        if (arr.length > THRESHOLD) {
            return ForkJoinTask.invokeAll(createSubtasks())
              .stream()
              .mapToInt(ForkJoinTask::join)
              .sum();
        } else {
            return processing(arr);
        }
    }

    private Collection createSubtasks() {
        List dividedTasks = new ArrayList<>();
        dividedTasks.add(new CustomRecursiveTask(
          Arrays.copyOfRange(arr, 0, arr.length / 2)));
        dividedTasks.add(new CustomRecursiveTask(
          Arrays.copyOfRange(arr, arr.length / 2, arr.length)));
        return dividedTasks;
    }

    private Integer processing(int[] arr) {
        return Arrays.stream(arr)
          .filter(a -> a > 10 && a < 27)
          .map(a -> a * 10)
          .sum();
    }
}

Dans cet exemple, le travail est représenté par un tableau stocké dans le champarr de la classeCustomRecursiveTask. La méthodecreateSubtask() divise récursivement la tâche en pièces de travail plus petites jusqu'à ce que chaque pièce soit inférieure au seuil. Ensuite, la méthodeinvokeAll() soumet les sous-tâches à l'extraction commune et renvoie une liste deFuture.

Pour déclencher l'exécution, la méthodejoin() est appelée pour chaque sous-tâche.

Dans cet exemple, cela est accompli en utilisantStream API; de Java 8, la méthodesum() est utilisée comme représentation de la combinaison de sous-résultats dans le résultat final.

4. Soumettre des tâches auxForkJoinPool

Pour soumettre des tâches au pool de threads, peu d'approches peuvent être utilisées.

La méthodesubmit() ouexecute() (leurs cas d'utilisation sont les mêmes):

forkJoinPool.execute(customRecursiveTask);
int result = customRecursiveTask.join();

La méthodeinvoke() divise la tâche et attend le résultat, et ne nécessite aucune jointure manuelle:

int result = forkJoinPool.invoke(customRecursiveTask);

La méthodeinvokeAll() est le moyen le plus pratique de soumettre une séquence deForkJoinTasks auForkJoinPool. Elle prend les tâches comme paramètres (deux tâches, var args, ou une collection), les fourche renvoie un collection d'objetsFuture dans l'ordre dans lequel ils ont été produits.

Vous pouvez également utiliser des méthodesfork() and join() distinctes. La méthodefork() soumet une tâche à un pool, mais elle ne déclenche pas son exécution. La méthodejoin() est utilisée à cet effet. Dans le cas deRecursiveAction, lejoin() ne renvoie rien d'autre quenull; pourRecursiveTask<V>,, il renvoie le résultat de l’exécution de la tâche:

customRecursiveTaskFirst.fork();
result = customRecursiveTaskLast.join();

Dans notre exempleRecursiveTask<V>, nous avons utilisé la méthodeinvokeAll() pour soumettre une séquence de sous-tâches au pool. Le même travail peut être effectué avecfork() etjoin(), bien que cela ait des conséquences sur l'ordre des résultats.

Pour éviter toute confusion, c'est généralement une bonne idée d'utiliser la méthodeinvokeAll() pour soumettre plus d'une tâche auxForkJoinPool.

5. Conclusions

L'utilisation du framework fork / join peut accélérer le traitement de tâches volumineuses, mais pour obtenir ce résultat, il convient de suivre certaines directives:

  • Use as few thread pools as possible - dans la plupart des cas, la meilleure décision est d'utiliser un pool de threads par application ou système

  • Use the default common thread pool, si aucun réglage spécifique n'est nécessaire

  • Use a reasonable threshold pour fractionnerForkJoingTask en sous-tâches

  • Évitez tout blocage dans votre ForkJoingTâches

Les exemples utilisés dans cet article sont disponibles dans leslinked GitHub repository.