web-dev-qa-db-fra.com

Exécuteurs Java: comment être averti, sans bloquer, quand une tâche est terminée

Dites que j'ai une file d'attente pleine de tâches que je dois soumettre à un service d'exécution. Je veux qu'ils soient traités un à la fois. Le moyen le plus simple auquel je puisse penser est de:

  1. Prendre une tâche de la file d'attente
  2. Le soumettre à l'exécuteur testamentaire
  3. Appelez .get sur le futur renvoyé et bloquez jusqu'à ce qu'un résultat soit disponible
  4. Prendre une autre tâche de la file d'attente ...

Cependant, j'essaie d'éviter de bloquer complètement. Si j'ai 10 000 files d'attente de ce type, dont les tâches doivent être traitées une à la fois, je manquerai d'espace dans la pile, car la plupart d'entre elles conserveront des threads bloqués. 

Ce que je voudrais, c'est soumettre une tâche et fournir un rappel qui est appelé lorsque la tâche est terminée. J'utiliserai cette notification de rappel comme indicateur pour envoyer la tâche suivante. (functionaljava et jetlang utilisent apparemment de tels algorithmes, mais je ne comprends pas leur code)

Comment puis-je faire cela en utilisant Java.util.concurrent de JDK, à moins d'écrire mon propre service exécuteur?

(la file d'attente qui me nourrit ces tâches peut elle-même bloquer, mais c'est un problème à résoudre plus tard)

121
Shahbaz

Définissez une interface de rappel pour recevoir les paramètres que vous souhaitez transmettre dans la notification d'achèvement. Puis appelez-le à la fin de la tâche. 

Vous pouvez même écrire un wrapper général pour les tâches Runnable et les soumettre à ExecutorService. Ou, voir ci-dessous un mécanisme intégré à Java 8.

class CallbackTask implements Runnable {

  private final Runnable task;

  private final Callback callback;

  CallbackTask(Runnable task, Callback callback) {
    this.task = task;
    this.callback = callback;
  }

  public void run() {
    task.run();
    callback.complete();
  }

}

Avec CompletableFuture , Java 8 incluait un moyen plus élaboré de composer des pipelines dans lesquels les processus peuvent être terminés de manière asynchrone et conditionnelle. Voici un exemple de notification artificiel mais complet.

import Java.util.concurrent.CompletableFuture;
import Java.util.concurrent.ThreadLocalRandom;
import Java.util.concurrent.TimeUnit;

public class GetTaskNotificationWithoutBlocking {

  public static void main(String... argv) throws Exception {
    ExampleService svc = new ExampleService();
    GetTaskNotificationWithoutBlocking listener = new GetTaskNotificationWithoutBlocking();
    CompletableFuture<String> f = CompletableFuture.supplyAsync(svc::work);
    f.thenAccept(listener::notify);
    System.out.println("Exiting main()");
  }

  void notify(String msg) {
    System.out.println("Received message: " + msg);
  }

}

class ExampleService {

  String work() {
    sleep(7000, TimeUnit.MILLISECONDS); /* Pretend to be busy... */
    char[] str = new char[5];
    ThreadLocalRandom current = ThreadLocalRandom.current();
    for (int idx = 0; idx < str.length; ++idx)
      str[idx] = (char) ('A' + current.nextInt(26));
    String msg = new String(str);
    System.out.println("Generated message: " + msg);
    return msg;
  }

  public static void sleep(long average, TimeUnit unit) {
    String name = Thread.currentThread().getName();
    long timeout = Math.min(exponential(average), Math.multiplyExact(10, average));
    System.out.printf("%s sleeping %d %s...%n", name, timeout, unit);
    try {
      unit.sleep(timeout);
      System.out.println(name + " awoke.");
    } catch (InterruptedException abort) {
      Thread.currentThread().interrupt();
      System.out.println(name + " interrupted.");
    }
  }

  public static long exponential(long avg) {
    return (long) (avg * -Math.log(1 - ThreadLocalRandom.current().nextDouble()));
  }

}
125
erickson

En Java 8, vous pouvez utiliser CompletableFuture . Voici un exemple dans mon code où je l'utilise pour extraire des utilisateurs de mon service utilisateur, les mapper sur mes objets de vue, puis mettre à jour ma vue ou afficher une boîte de dialogue d'erreur (il s'agit d'une application à interface graphique): 

    CompletableFuture.supplyAsync(
            userService::listUsers
    ).thenApply(
            this::mapUsersToUserViews
    ).thenAccept(
            this::updateView
    ).exceptionally(
            throwable -> { showErrorDialogFor(throwable); return null; }
    );

Il s'exécute de manière asynchrone. J'utilise deux méthodes privées: mapUsersToUserViews et updateView.

47
Matt

Utilisez la future API écoutable de Guava et ajoutez un rappel. Cf. sur le site:

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
  public Explosion call() {
    return pushBigRedButton();
  }
});
Futures.addCallback(explosion, new FutureCallback<Explosion>() {
  // we want this handler to run immediately after we Push the big red button!
  public void onSuccess(Explosion explosion) {
    walkAwayFrom(explosion);
  }
  public void onFailure(Throwable thrown) {
    battleArchNemesis(); // escaped the explosion!
  }
});
46
Pierre-Henri

Vous pouvez étendre la classe FutureTask et remplacer la méthode done(), puis ajouter l'objet FutureTask à la ExecutorService, de sorte que la méthode done() sera appelée lorsque la FutureTask sera terminée immédiatement.

24
Auguste

ThreadPoolExecutor possède également des méthodes de raccordement beforeExecute et afterExecute que vous pouvez remplacer et utiliser. Voici la description de ThreadPoolExecutor 's Javadocs .

Méthodes de crochet

Cette classe fournit les méthodes beforeExecute(Java.lang.Thread, Java.lang.Runnable) et afterExecute(Java.lang.Runnable, Java.lang.Throwable) qui sont protégées et qui sont appelées avant et après l'exécution de chaque tâche. Ceux-ci peuvent être utilisés pour manipuler l'environnement d'exécution; par exemple, réinitialiser ThreadLocals, rassembler des statistiques ou ajouter des entrées de journal. En outre, la méthode terminated() peut être remplacée pour effectuer tout traitement spécial à effectuer une fois que la variable Executor est complètement terminée. Si les méthodes de raccordement ou de rappel génèrent des exceptions, les threads de travail internes peuvent à leur tour échouer et se terminer brutalement.

14
Cem Catikkas

Utilisez un CountDownLatch .

C'est à partir de Java.util.concurrent et c'est exactement le moyen d'attendre l'exécution de plusieurs threads avant de continuer.

Pour obtenir l'effet de rappel que vous recherchez, cela nécessite un peu de travail supplémentaire. C'est-à-dire que vous gérez cela par vous-même dans un thread séparé qui utilise la variable CountDownLatch et l'attend, puis continue à notifier tout ce que vous devez notifier. Il n'y a pas de support natif pour le rappel, ou quelque chose de similaire à cet effet.


EDIT: maintenant que je comprends mieux votre question, je pense que vous allez trop loin, inutilement. Si vous prenez un SingleThreadExecutor normal, donnez-lui toutes les tâches, et la mise en file d'attente se fera de manière native.

6
Yuval Adam

Si vous voulez vous assurer qu'aucune tâche ne s'exécutera en même temps, utilisez un SingleThreadedExecutor . Les tâches seront traitées dans l'ordre dans lequel elles sont soumises. Vous n'avez même pas besoin de garder les tâches, il vous suffit de les soumettre à l'exécutif.

4
basszero

Code simple pour implémenter le mécanisme Callback à l'aide de ExecutorService

import Java.util.concurrent.*;
import Java.util.*;

public class CallBackDemo{
    public CallBackDemo(){
        System.out.println("creating service");
        ExecutorService service = Executors.newFixedThreadPool(5);

        try{
            for ( int i=0; i<5; i++){
                Callback callback = new Callback(i+1);
                MyCallable myCallable = new MyCallable((long)i+1,callback);
                Future<Long> future = service.submit(myCallable);
                //System.out.println("future status:"+future.get()+":"+future.isDone());
            }
        }catch(Exception err){
            err.printStackTrace();
        }
        service.shutdown();
    }
    public static void main(String args[]){
        CallBackDemo demo = new CallBackDemo();
    }
}
class MyCallable implements Callable<Long>{
    Long id = 0L;
    Callback callback;
    public MyCallable(Long val,Callback obj){
        this.id = val;
        this.callback = obj;
    }
    public Long call(){
        //Add your business logic
        System.out.println("Callable:"+id+":"+Thread.currentThread().getName());
        callback.callbackMethod();
        return id;
    }
}
class Callback {
    private int i;
    public Callback(int i){
        this.i = i;
    }
    public void callbackMethod(){
        System.out.println("Call back:"+i);
        // Add your business logic
    }
}

sortie:

creating service
Callable:1:pool-1-thread-1
Call back:1
Callable:3:pool-1-thread-3
Callable:2:pool-1-thread-2
Call back:2
Callable:5:pool-1-thread-5
Call back:5
Call back:3
Callable:4:pool-1-thread-4
Call back:4

Notes clés:

  1. Si vous souhaitez traiter les tâches en séquence dans l'ordre FIFO, remplacez newFixedThreadPool(5) par newFixedThreadPool(1)
  2. Si vous souhaitez traiter la tâche suivante après avoir analysé le résultat de callback de la tâche précédente, supprimez simplement le commentaire sous la ligne.

    //System.out.println("future status:"+future.get()+":"+future.isDone());
    
  3. Vous pouvez remplacer newFixedThreadPool() par l’un des 

    Executors.newCachedThreadPool()
    Executors.newWorkStealingPool()
    ThreadPoolExecutor
    

    en fonction de votre cas d'utilisation. 

  4. Si vous voulez gérer la méthode de rappel de manière asynchrone

    une. Passer un ExecutorService or ThreadPoolExecutor partagé à la tâche appelable

    b. Convertissez votre méthode Callable en tâche Callable/Runnable

    c. Pousser la tâche de rappel à ExecutorService or ThreadPoolExecutor

1
Ravindra babu

Ceci est une extension de la réponse de Pache en utilisant la variable ListenableFuture de Guava.

En particulier, Futures.transform() renvoie ListenableFuture et peut donc être utilisé pour chaîner des appels asynchrones. Futures.addCallback() renvoie void, ne peut donc pas être utilisé pour le chaînage, mais convient pour gérer le succès/l'échec d'une exécution asynchrone.

// ListenableFuture1: Open Database
ListenableFuture<Database> database = service.submit(() -> openDatabase());

// ListenableFuture2: Query Database for Cursor rows
ListenableFuture<Cursor> cursor =
    Futures.transform(database, database -> database.query(table, ...));

// ListenableFuture3: Convert Cursor rows to List<Foo>
ListenableFuture<List<Foo>> fooList =
    Futures.transform(cursor, cursor -> cursorToFooList(cursor));

// Final Callback: Handle the success/errors when final future completes
Futures.addCallback(fooList, new FutureCallback<List<Foo>>() {
  public void onSuccess(List<Foo> foos) {
    doSomethingWith(foos);
  }
  public void onFailure(Throwable thrown) {
    log.error(thrown);
  }
});

REMARQUE: Outre le chaînage de tâches asynchrones, Futures.transform() vous permet également de planifier chaque tâche sur un exécuteur distinct (non illustré dans cet exemple).

1
bcorso

Pour ajouter à la réponse de Matt, ce qui a aidé, voici un exemple plus étoffé pour montrer l'utilisation d'un rappel.

private static Primes primes = new Primes();

public static void main(String[] args) throws InterruptedException {
    getPrimeAsync((p) ->
        System.out.println("onPrimeListener; p=" + p));

    System.out.println("Adios mi amigito");
}
public interface OnPrimeListener {
    void onPrime(int prime);
}
public static void getPrimeAsync(OnPrimeListener listener) {
    CompletableFuture.supplyAsync(primes::getNextPrime)
        .thenApply((prime) -> {
            System.out.println("getPrimeAsync(); prime=" + prime);
            if (listener != null) {
                listener.onPrime(prime);
            }
            return prime;
        });
}

La sortie est:

    getPrimeAsync(); prime=241
    onPrimeListener; p=241
    Adios mi amigito
1
Old Jack

Vous pouvez utiliser une implémentation de Callable telle que 

public class MyAsyncCallable<V> implements Callable<V> {

    CallbackInterface ci;

    public MyAsyncCallable(CallbackInterface ci) {
        this.ci = ci;
    }

    public V call() throws Exception {

        System.out.println("Call of MyCallable invoked");
        System.out.println("Result = " + this.ci.doSomething(10, 20));
        return (V) "Good job";
    }
}

où CallbackInterface est quelque chose de très basique comme 

public interface CallbackInterface {
    public int doSomething(int a, int b);
}

et maintenant la classe principale ressemblera à ceci 

ExecutorService ex = Executors.newFixedThreadPool(2);

MyAsyncCallable<String> mac = new MyAsyncCallable<String>((a, b) -> a + b);
ex.submit(mac);
0
Deepika Anand