Exécuteurs Java: comment être notifié, sans blocage, lorsqu'une tâche se termine?


154

Disons que j'ai une file d'attente pleine de tâches que je dois soumettre à un service d'exécuteur testamentaire. 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. Soumettez-le à l'exécuteur testamentaire
  3. Appelez .get sur le Future retourné et bloquez jusqu'à ce qu'un résultat soit disponible
  4. Prenez une autre tâche de la file d'attente ...

Cependant, j'essaie d'éviter de bloquer complètement. Si j'ai 10000 de ces files d'attente, qui nécessitent que leurs tâches soient traitées une par une, je vais manquer d'espace dans la pile, car la plupart d'entre elles conserveront les 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 non bloquants, mais je ne comprends pas leur code)

Comment puis-je faire cela en utilisant java.util.concurrent de JDK, sans écrire mon propre service exécuteur?

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

Réponses:


146

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

Vous pouvez même écrire un wrapper général pour les tâches exécutables et les soumettre à ExecutorService. Ou, voir ci-dessous pour 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 où les processus peuvent être exécutés de manière asynchrone et conditionnelle. Voici un exemple artificiel mais complet de notification.

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()));
  }

}

1
Trois réponses en un clin d'œil! J'aime la CallbackTask, une solution si simple et directe. Cela semble évident rétrospectivement. Merci. Concernant les autres commentaires sur SingleThreadedExecutor: je peux avoir des milliers de files d'attente qui peuvent avoir des milliers de tâches. Chacun d'eux doit traiter ses tâches une par une, mais différentes files d'attente peuvent fonctionner en parallèle. C'est pourquoi j'utilise un seul threadpool global. Je suis nouveau pour les exécuteurs testamentaires, alors dites-moi si je me trompe.
Shahbaz

5
Bon modèle, j'utiliserais cependant la future API écoutable de Guava qui en fournit une très bonne implémentation.
Pierre-Henri

cela ne bat-il pas l'objectif de l'utilisation de Future?
takecare

2
@Zelphir C'était une Callbackinterface que vous déclarez; pas d'une bibliothèque. De nos jours, j'utiliserais probablement justeRunnable , Consumerou BiConsumer, selon ce que je dois passer en arrière de la tâche à l'auditeur.
erickson le

1
@Bhargav Ceci est typique des callbacks - une entité externe "rappelle" l'entité de contrôle. Voulez-vous que le thread qui a créé la tâche se bloque jusqu'à ce que la tâche soit terminée? Alors quel est le but de l'exécution de la tâche sur un deuxième thread? Si vous autorisez le thread à continuer, il devra vérifier à plusieurs reprises un état partagé (probablement dans une boucle, mais dépend de votre programme) jusqu'à ce qu'il remarque une mise à jour (indicateur booléen, nouvel élément dans la file d'attente, etc.) effectuée par le vrai rappel comme décrit dans cette réponse. Il peut alors effectuer des travaux supplémentaires.
erickson

52

Dans Java 8, vous pouvez utiliser CompletableFuture . Voici un exemple que j'ai eu dans mon code où je l'utilise pour récupérer les utilisateurs de mon service utilisateur, les mapper à mes objets de vue, puis mettre à jour ma vue ou afficher une boîte de dialogue d'erreur (il s'agit d'une application GUI):

    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: mapUsersToUserViewset updateView.


Comment utiliserait-on un CompletableFuture avec un exécuteur testamentaire? (pour limiter le nombre d'instances concurrentes / parallèles) Serait-ce un indice: cf: soumettant-des-futuresetasks-à-un-exécuteur-pourquoi-ça-marche ?
user1767316

47

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

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!
  }
});

salut, mais si je veux m'arrêter après avoir réussi ce fil, comment puis-je faire?
DevOps85

24

Vous pouvez étendre la FutureTaskclasse et redéfinir la done()méthode, puis ajouter l' FutureTaskobjet au ExecutorService, de sorte que la done()méthode s'appellera une fois l'opération FutureTaskterminée immédiatement.


then add the FutureTask object to the ExecutorService, pourriez-vous s'il vous plaît me dire comment faire cela?
Gary Gauh

@GaryGauh voir ceci pour plus d'informations, vous pouvez étendre FutureTask, nous pouvons l'appeler MyFutureTask. Ensuite, utilisez ExcutorService pour soumettre MyFutureTask, puis la méthode d'exécution de MyFutureTask s'exécutera, lorsque MyFutureTask aura terminé votre méthode done sera appelée.Ici, quelque chose de déroutant est deux FutureTask, et en fait MyFutureTask est un Runnable normal.
Chaojun Zhong

15

ThreadPoolExecutora également beforeExecuteet des afterExecuteméthodes hook que vous pouvez remplacer et utiliser. Voici la description ThreadPoolExecutordes Javadocs de .

Méthodes de crochet

Cette classe fournit des méthodes beforeExecute(java.lang.Thread, java.lang.Runnable)et des afterExecute(java.lang.Runnable, java.lang.Throwable)méthodes remplaçables protégées 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, collecter 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 qui doit être effectué une fois que le Executorest complètement terminé. Si les méthodes de hook ou de rappel lèvent des exceptions, les threads de travail internes peuvent à leur tour échouer et s'arrêter brusquement.


6

Utilisez un CountDownLatch.

C'est de java.util.concurrent et c'est exactement la façon d'attendre que plusieurs threads se terminent l'exécution avant de continuer.

Afin d'obtenir l'effet de rappel que vous recherchez, cela nécessite un peu de travail supplémentaire. À savoir, le gérer vous-même dans un thread séparé qui utilise le CountDownLatchet l'attend, puis continue à notifier 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 régulier SingleThreadExecutor, donnez-lui toutes les tâches, et il fera la file d'attente de manière native.


En utilisant SingleThreadExecutor, quel est le meilleur moyen de savoir que tous les threads sont terminés? J'ai vu un exemple qui utilise un moment! Executor.isTerminated mais cela ne semble pas très élégant. J'ai implémenté une fonction de rappel pour chaque travailleur et incrémenter un nombre qui fonctionne.
Ours le

5

Si vous souhaitez 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 où elles sont soumises. Vous n'avez même pas besoin de tenir les tâches, soumettez-les simplement à l'exécutif.


2

Code simple pour implémenter le Callbackmécanisme en utilisantExecutorService

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
    }
}

production:

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 dans l'ordre dans l'ordre FIFO, remplacez newFixedThreadPool(5) parnewFixedThreadPool(1)
  2. Si vous souhaitez traiter la tâche suivante après avoir analysé le résultat callbackde la tâche précédente, annulez 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 souhaitez gérer la méthode de rappel de manière asynchrone

    une. Passer une ExecutorService or ThreadPoolExecutortâche partagée à appelable

    b. Convertissez votre Callableméthode en Callable/Runnabletâche

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


1

Juste pour ajouter à la réponse de Matt, 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

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);

1

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

En particulier, les Futures.transform()retours ListenableFutureainsi peuvent être utilisés pour enchaîner les appels asynchrones. Futures.addCallback()renvoie void, donc ne peut pas être utilisé pour le chaînage, mais est bon pour gérer le succès / échec lors d'un achèvement 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 des tâches asynchrones, Futures.transform()vous permet également de planifier chaque tâche sur un exécuteur distinct (non illustré dans cet exemple).


Cela semble plutôt sympa.
kaiser
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.