Aller au contenu

Thread avec valeur de retour (Callable)

Un thread standard ne retourne aucune valeur. Il s'exécute, fait son travail et se termine. Cependant, il pourrait être pratique d'obtenir une valeur de retour à partir d'un thread. Par exemple, si on voulait calculer plusieurs très grands nombres premiers simultanément ou bien d'aller chercher plusieurs valeurs sur plusieurs sites internet, le résultat serait important. Deux morceaux seront alors requis : les Callable et les ExecutorService.

L'interface Callable

Un thread standard implémente l'interface Runnable et remplace la méthode run() pour s'exécuter. Cette méthode ne retourne rien (type void). Un thread avec valeur de retour est très semblable, à quelques différences près :

  • Elle implémente l'interface Callable<T> au lieu de Runnable
  • Elle remplace la méthode <T> call() au lieu de void run()

Note

Lorsque <T> (ou <K, V>, <E>, etc.) est utilisé dans la documentation, ça signifie que le type sera variable et dépendant des besoins du programmeur. Par exemple, la signature d'un ArrayList est ArrayList<E> (ex. new ArrayList<Integer>()) et celle d'un HashMap est HashMap<K, V> (ex. new HashMap<String, Integer>()).

Ici est un thread qui calcule le N nombre premier.

CalculerPremier.java
public class CalculerPremier implements Runnable {

    private int lequel = 1;

    public CalculerPremier(Integer lequel) {
        this.lequel = lequel;
    }

    @Override
    public void run() {
        // Calculer le N nombre premier
        int premier = 2;
        while (lequel > 0) {
            for (int i = 2; i <= premier/2; i++) {
                if (premier % i == 0) {
                    lequel++;
                    break;
                }
            }
            lequel--;
            premier++;
        }

        // Le N nombre premier a été trouvé
        System.out.println(--premier);
    }
}

Malheureusement, le nombre premier doit est affiché à l'écran et ne peut pas être utilisé à l'extérieur. Pour transformer ce thread en Callable, il suffit de remplacer son implémentation :

CalculerPremier.java
import java.util.concurrent.Callable;

public class CalculerPremier implements Callable<Integer> {

    private int lequel = 1;

    public CalculerPremier(Integer lequel) {
        this.lequel = lequel;
    }

    @Override
    public Integer call() {
        // Calculer le N nombre premier
        int premier = 2;
        while (lequel > 0) {
            for (int i = 2; i <= premier/2; i++) {
                if (premier % i == 0) {
                    lequel++;
                    break;
                }
            }
            lequel--;
            premier++;
        }

        // Le N nombre premier a été trouvé
        return --premier;
    }
}

La classe est maintenant prête à être appelée par un ExecutorService.

La classe ExecutorService

Un exécuteur permet d'exécuter un ou plusieurs threads simultanément. Il en gère l'exécution et la quantité à déployer d'un même coup. Un pool de 5 lancera au maximum 5 threads à la fois, jamais plus.

Il existe plusieurs façons de créer un ExecutorService :

// Créer un service d'un seul thread
ExecutorService executorService = Executors.newSingleThreadExecutor();

// Créer un service avec un pool de 5 threads
ExecutorService executorService = Executors.newFixedThreadPool(5);

// Créer un service avec un pool de 1 threads (équivalent de newSingleThreadExecutor())
ExecutorService executorService = Executors.newFixedThreadPool(1);

On peut lancer les threads un par un avec la méthode submit() ou plusieurs à la fois s'ils sont mis dans un ArrayList().

Lancer un thread avec submit()

Pour lancer un thread un par un, la méthode submit() retournera un objet de type Future (semblable à une Promise en javascript). C'est cet objet qui nous donnera la valeur de retour lorsqu'elle sera disponible.

try (ExecutorService executorService = Executors.newFixedThreadPool(5)) {
    int position = 10;

    // Lance le thread
    Future<Integer> future = executorService.submit(new CalculerPremier(position));

    // Attend pour le retour du thread
    int nombrePremier = future.get();

    // Affiche le résultat
    System.out.printf("Le nombre premier #%d est %d.\n", position, nombrePremier);
} catch (ExecutionException | InterruptedException e) {
    throw new RuntimeException(e);
}

Lancer plusieurs threads avec invokeAll()

Il est aussi possible de donner une quantité de threads à lancer à un ExecutorService et lui laisser gérer leur lancement.

ArrayList<CalculerPremier> premierArrayList = new ArrayList<>();

// Créer 40 threads
for (int i = 10; i < 50; i++) {
    premierArrayList.add(new CalculerPremier(i));
}

try (ExecutorService executorService = Executors.newFixedThreadPool(5)) {
    List<Future<Integer>> futureList;

    // Lancer tous les threads jusqu'a un maximum de 5 simultanés
    futureList = executorService.invokeAll(premierArrayList);

    // Afficher les résultats lorsque disponible
    for (Future<Integer> future : futureList) {
        System.out.println(future.get());
    }
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
}

En lançant le programme, on remarquera que l'affichage attend que tous les threads soient terminés avant de s'afficher. À la ligne 12, la méthode invokeAll() attend que tous les threads aient terminés avant de poursuivre.

Note

Pour confirmer que les threads démarrent par bloc de 5, ajoutez un System.out.println() à la méthode call(). Vous verrez alors des messages par bloc de 5.