“Future”, le Parallel Rendering du pauvre

Peut-être avez vous déjà entendu parler de lift, le framework web Scala. Peut-être avez vous déjà entendu parler des 7 choses qui font que lift est “le plus puissant des framework web d’aujourd’hui”. Et si tel est le cas, vous connaissez sûrement le rendu de pages parallélisé

Révolutionnaire ? À mon avis, pas tant que ça. En tout cas, pas techniquement. L’excellente API java.util.concurrent présente depuis le JDK 1.5 propose des outils très intéressants pour les traitements asynchrones, le multi-threading, etc. L’interface qui m’intéresse aujourd’hui est java.util.concurrent.Future

Si vous avez bonne mémoire, le livre d’entrainement à la SCJP possède un chapitre sur les Threads (le 9 de mémoire), mais reste malgré tout assez “bas niveau”. L’API concurrent dans les grandes lignes se résume à ses interfaces, desquelles je vous conseille une lecture de la javadoc. Nous nous interesserons ici à :

  • Executor et son fils ThreadPoolExecutor
  • Future, et sa fille FutureTask

Future, mais pourquoi faire ?

Comme dans lift, nous allons générer des bouts de pages. La première approche, celle de tous les jours, est séquentielle : Attendre que le premier traitement soit terminé pour commencer le 2ème. Ainsi, le temps total passé à générer est égal à la somme des traitements unitaires.

La deuxième approche, celle de Lift est plus intelligente et met à profit nos serveurs remplis de Bi-CPU Quad-core : elle commence les traitements en même temps et attend que les traitements soient terminés pour assembler le résultat. Ainsi, le temps sera, dans le meilleur des cas, égal au temps du traitement le plus long auquel vient s’ajouter le temps d’assemblage.

Jusqu’ici, tout va bien. Mais une question reste en suspens : Comment faire pour récupérer la valeur qui va être calculée dans mon thread courant ? C’est là qu’intervient Future. Comme son nom l’indique, il s’agit d’une valeur future. Ainsi, on peut la garder sous le coude et commencer d’autres traitements jusqu’à ce qu’on ait vraiment besoin de sa valeur.

Exemple

J’ai choisi un traitement simple et visuel pour illustrer mon propos : Renverser les caractères d’une chaine :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static class SnailStringReverser {
        public String reverse(String str) {
            String result = "";

            for (int i = str.length() - 1; i >= 0; i--) {
                result += str.charAt(i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    //ugly but useless in the test...
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + " : " + result);
            }

            return result;
        }
    }

Je l’ai volontairement ralenti en utilisant des Thread.sleep. L’idée est de construire une FutureTask anonyme qui va déléguer le traitement à mon SnailStringReverser. Puis, une fois cette FutureTask construite, je vais la soumettre à l’executor et essayer de récupérer son résultat :

1
2
3
4
5
6
7
8
9
10
11
12
13
    protected static FutureTask computeReversedString(final String input) {
        final SnailStringReverser snailStringReverser = new SnailStringReverser();

        FutureTask futureValue = new FutureTask(new Callable() {
            @Override
            public String call() throws Exception {
                return snailStringReverser.reverse(input);
            }
        });

        executor.submit(futureValue);
        return futureValue;
    }

Voyons maintenant quelques une des méthodes disponibles sur Future :

  • get() : C’est la base de future. C’est de cette manière qu’on récupère la donnée. Elle est synchrone, c’est à dire qu’elle retournera le résultat une fois le calcul terminé.
  • get(long timeout, TimeUnit unit) : C’est la même méthode que get(), cependant, si le timeout est dépassé, la méthode lance une exception : TimeOutException. Le traitement n’en est pas arrêté pour autant.
  • isDone() : Pas besoin d’un dessin !

Voici un petit exemple d’utilisation :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        FutureTask futureValue = computeReversedString("Was it a rat I saw?");
        FutureTask futureValue1 = computeReversedString("A nut for a jar of tuna");
        FutureTask futureValue2 = computeReversedString("Dammit, I'm mad!");
        FutureTask futureValue3 = computeReversedString("Madam in Eden, I'm Adam");
        FutureTask futureValue4 = computeReversedString("A man, a plan, a canal, Panama");

        try {
            System.out.println("I can't wait, what is the reversed string ?");
            System.out.println("Finished 1 : " + futureValue1.get(5, TimeUnit.SECONDS));
        } catch (ExecutionException e) {

        } catch (TimeoutException e) {
            System.out.println("Ok, let's go to sleep...");
            Thread.sleep(5000);
        }
           

        try {
            System.out.println("at : "+(System.currentTimeMillis()-start )+" Finished 0 : " + futureValue.get());
            System.out.println("at : "+(System.currentTimeMillis()-start )+" Finished 1 : " + futureValue1.get());
            System.out.println("at : "+(System.currentTimeMillis()-start )+" Finished 2 : " + futureValue2.get());
            System.out.println("at : "+(System.currentTimeMillis()-start )+" Finished 3 : " + futureValue3.get());
            System.out.println("at : "+(System.currentTimeMillis()-start )+" Finished 4 : " + futureValue4.get());
        } catch (ExecutionException e) {

        }


    }

Si vous avez bien suivi, le dernier point à traiter est ce qui se cache derrière l’executor. Il s’agit du pool de Thread qui traitera les tâches. Voici ma configuration :

  • 10 Threads, à garder tout le temps, même s’ils sont idles
  • une durée de vie dans le pool de 500ms
  • une file d’attente de 100 éléments.

Et sa déclaration :

1
static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 500, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100));

Et au niveau des logs :

I can't wait, what is the reversed string ?
Thread[pool-1-thread-1,5,main] : ?
Thread[pool-1-thread-2,5,main] : a
Thread[pool-1-thread-3,5,main] : !
Thread[pool-1-thread-4,5,main] : m
Thread[pool-1-thread-5,5,main] : a
Thread[pool-1-thread-1,5,main] : ?w
...
[ndla : 5 secondes passent, le thread principal est bloqué sur
futureValue1.get(5, TimeUnit.SECONDS)]
Thread[pool-1-thread-1,5,main] : ?was I tar
Ok, let's go to sleep...
[ndla : 5 secondes passent, le thread principal est endormi]
...
[ndla : Et le premier résultat arrive !]
at : 10045 Finished 0 : ?was I tar a ti saW
Thread[pool-1-thread-5,5,main] : amanaP ,lanac a ,nalp
...
Thread[pool-1-thread-2,5,main] : anut fo raj a rof tun A
at : 10045 Finished 1 : anut fo raj a rof tun A
at : 11579 Finished 2 : !dam m'I ,timmaD
Thread[pool-1-thread-5,5,main] : amanaP ,lanac a ,nalp a
Thread[pool-1-thread-4,5,main] : madA m'I ,nedE ni madaM
at : 11579 Finished 3 : madA m'I ,nedE ni madaM
[ndla le thread attend la dernière valeur (futureValue4)]
...
Thread[pool-1-thread-5,5,main] : amanaP ,lanac a ,nalp a ,nam A
at : 11616 Finished 4 : amanaP ,lanac a ,nalp a ,nam A

Pour aller plus loin

Dans la version 3.0 de Spring (module context), vous l’aurez peut-être remarqué, il y a l’annotation @Async . Et transformer un traitement synchrone en traitement asynchrone n’aura jamais été aussi simple :

1
2
3
4
@Async
Future returnSomething(int i) {
    // this will be executed asynchronously
}
VN:R_U [1.9.22_1171]
Rating: +1 (from 1 vote)
Share
Ce contenu a été publié dans Outils, Spring, Trucs & astuces, avec comme mot(s)-clef(s) , , , , . Vous pouvez le mettre en favoris avec ce permalien.

3 réponses à “Future”, le Parallel Rendering du pauvre

  1. Tu fais bien de rappeler que le traitement n’est pas interrompu :-) . Future ne doit pas être utilisé pour créer des traitements à durée max, dont le but serait de limiter le CPU / temps autorisé pour une opération afin de ne pas surcharger une VM. D’ailleurs, à ce sujet, je ne connais pas de solution. Any hint ?

    Je m’interroge sur la facilité d’utilisation d’@Async en terme d’API… en effet, on doit créer la future dans notre code, pour pouvoir la retourner. A priori, ça implique de créer une Future qui ne sert pas à exécuter du code, mais juste à retourner une valeur. Est-ce une classe du genre “FutureValueHolder” implementant Future existe ? Ça donnerait ça :

    1
    2
    3
    4
    5
    @Async
    Future<String> returnSomething(int i) {
        // this will be executed asynchronously
        return new FutureValueHolder<String>("MyComputedValue");
    }
    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
  2. Bon bah ça existe bien, et ça s’appelle ASyncResult :

    1
    2
    3
    4
    5
    @Async
    public Future<Balance> findBalanceAsync(final Account account) {
        Balance balance = accountRepository.findBalance(account);
        return new AsyncResult<Balance>(balance);
    }
    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)

Laisser un commentaire