web-dev-qa-db-fra.com

ExecutorService qui interrompt les tâches après un délai d'attente

Je recherche une implémentation ExecutorService pouvant être fournie avec un délai d'attente. Les tâches soumises à ExecutorService sont interrompues si leur exécution prend plus de temps que le délai imparti. Implémenter une telle bête n’est pas une tâche aussi difficile, mais je me demande si quelqu'un connaît une implémentation existante.

Voici ce que j'ai trouvé sur la base de certaines des discussions ci-dessous. Des commentaires?

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

public class TimeoutThreadPoolExecutor extends ThreadPoolExecutor {
    private final long timeout;
    private final TimeUnit timeoutUnit;

    private final ScheduledExecutorService timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
    private final ConcurrentMap<Runnable, ScheduledFuture> runningTasks = new ConcurrentHashMap<Runnable, ScheduledFuture>();

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    public TimeoutThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler, long timeout, TimeUnit timeoutUnit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        this.timeout = timeout;
        this.timeoutUnit = timeoutUnit;
    }

    @Override
    public void shutdown() {
        timeoutExecutor.shutdown();
        super.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        timeoutExecutor.shutdownNow();
        return super.shutdownNow();
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        if(timeout > 0) {
            final ScheduledFuture<?> scheduled = timeoutExecutor.schedule(new TimeoutTask(t), timeout, timeoutUnit);
            runningTasks.put(r, scheduled);
        }
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        ScheduledFuture timeoutTask = runningTasks.remove(r);
        if(timeoutTask != null) {
            timeoutTask.cancel(false);
        }
    }

    class TimeoutTask implements Runnable {
        private final Thread thread;

        public TimeoutTask(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            thread.interrupt();
        }
    }
}
84
Edward Dale

Vous pouvez utiliser un ScheduledExecutorService pour cela. D'abord, vous le soumettriez une seule fois pour commencer immédiatement et conserver le futur créé. Après cela, vous pouvez soumettre une nouvelle tâche qui annulerait le futur retenu après un certain temps.

 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 
 final Future handler = executor.submit(new Callable(){ ... });
 executor.schedule(new Runnable(){
     public void run(){
         handler.cancel();
     }      
 }, 10000, TimeUnit.MILLISECONDS);

Ceci exécutera votre gestionnaire (fonctionnalité principale à interrompre) pendant 10 secondes, puis annulera (c'est-à-dire interrompra) cette tâche spécifique.

82
John Vint

Malheureusement, la solution est imparfaite. Il y a une sorte de bogue avec ScheduledThreadPoolExecutor, également signalé dans cette question : annuler une tâche soumise ne libère pas complètement les ressources de mémoire associées à la tâche; les ressources ne sont libérées qu'à l'expiration de la tâche.

Par conséquent, si vous créez un TimeoutThreadPoolExecutor avec un délai d’expiration assez long (utilisation typique) et soumettez les tâches assez rapidement, vous finissez par remplir la mémoire - même si les tâches se sont bien terminées.

Vous pouvez voir le problème avec le programme de test suivant (très grossier):

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = new TimeoutThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, 
            new LinkedBlockingQueue<Runnable>(), 10, TimeUnit.MINUTES);
    //ExecutorService service = Executors.newFixedThreadPool(1);
    try {
        final AtomicInteger counter = new AtomicInteger();
        for (long i = 0; i < 10000000; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    counter.incrementAndGet();
                }
            });
            if (i % 10000 == 0) {
                System.out.println(i + "/" + counter.get());
                while (i > counter.get()) {
                    Thread.sleep(10);
                }
            }
        }
    } finally {
        service.shutdown();
    }
}

Le programme épuise la mémoire disponible, bien qu'il attende que le Runnables engendré soit terminé.

J'y ai réfléchi pendant un moment, mais malheureusement, je n'ai pas pu trouver de bonne solution.

EDIT: J'ai découvert que ce problème avait été signalé comme bogue JDK 66026 et semble avoir été corrigé très récemment.

5
Flavio

Emballez la tâche dans FutureTask et vous pouvez spécifier un délai d'expiration pour FutureTask. Regardez l'exemple dans ma réponse à cette question,

délai d'attente du processus natif Java

4
ZZ Coder

Il semble que le problème ne se trouve pas dans le bogue JDK 6602600 (il a été résolu le 22/05/2010), mais dans un appel incorrect de sleep (10) en cercle. Remarque supplémentaire, le thread principal doit donner directement CHANCE aux autres threads pour réaliser leurs tâches en appelant SLEEP (0) dans CHAQUE branche du cercle extérieur. Il vaut mieux, je pense, utiliser Thread.yield () au lieu de Thread.sleep (0)

Le résultat corrigé dans le code du problème précédent est le suivant:

.......................
........................
Thread.yield();         

if (i % 1000== 0) {
System.out.println(i + "/" + counter.get()+ "/"+service.toString());
}

//                
//                while (i > counter.get()) {
//                    Thread.sleep(10);
//                } 

Cela fonctionne correctement avec la quantité de compteur externe jusqu’à 150 000 000 de cercles testés.

1
Sergey

Après une tonne de temps pour enquêter,
Enfin, j’utilise invokeAll méthode de ExecutorService pour résoudre ce problème.
Cela interrompra strictement la tâche en cours d'exécution.
Voici un exemple

ExecutorService executorService = Executors.newCachedThreadPool();

try {
    List<Callable<Object>> callables = new ArrayList<>();
    // Add your long time task (callable)
    callables.add(new VaryLongTimeTask());
    // Assign tasks for specific execution timeout (e.g. 2 sec)
    List<Future<Object>> futures = executorService.invokeAll(callables, 2000, TimeUnit.MILLISECONDS);
    for (Future<Object> future : futures) {
        // Getting result
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

executorService.shutdown();

Le pro est que vous pouvez aussi soumettre ListenableFuture en même temps ExecutorService.
Modifiez légèrement la première ligne de code.

ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

ListeningExecutorService est la fonctionnalité d'écoute de ExecutorService dans le projet google guava (com.google.guava))

1
Johnny

Pourquoi ne pas utiliser la méthode ExecutorService.shutDownNow() comme décrit dans http://docs.Oracle.com/javase/7/docs/api/Java/util/concurrent/ExecutorService.html ? Cela semble être la solution la plus simple.

1
Giovanni Botta

En utilisant John W answer, j'ai créé une implémentation qui commence correctement le délai d'expiration lorsque la tâche commence son exécution. J'ai même écrit un test unitaire pour ça :)

Cependant, cela ne convient pas à mes besoins car certaines opérations IO ne s'interrompent pas lorsque Future.cancel() est appelée (c'est-à-dire lorsque Thread.interrupted() est appelée)). Quelques exemples of IO) Une opération qui ne peut pas être interrompue lorsque Thread.interrupted() est appelée est Socket.connect et Socket.read (et je soupçonne que la plupart IO implémentée dans Java.io). Toutes les opérations IO dans Java.nio Devraient être interruptibles lorsque Thread.interrupted() est appelée Par exemple, c'est le cas pour SocketChannel.open Et SocketChannel.read.

Quoi qu'il en soit, si quelqu'un est intéressé, j'ai créé un Gist pour un exécuteur de pool de threads qui autorise le dépassement de délai des tâches (s'ils utilisent des opérations interruptibles ...): https://Gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf

1
amanteaux

Qu'en est-il de cette idée alternative:

  • deux ont deux exécuteurs:
    • un pour:
      • soumettre la tâche, sans se soucier du délai d'attente de la tâche
      • l'ajout de l'avenir résultant et le moment où il devrait se terminer à une structure interne
    • un pour l'exécution d'un travail interne qui vérifie la structure interne si certaines tâches ont expiré et si elles doivent être annulées.

Petit échantillon est ici:

public class AlternativeExecutorService 
{

private final CopyOnWriteArrayList<ListenableFutureTask> futureQueue       = new CopyOnWriteArrayList();
private final ScheduledThreadPoolExecutor                scheduledExecutor = new ScheduledThreadPoolExecutor(1); // used for internal cleaning job
private final ListeningExecutorService                   threadExecutor    = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5)); // used for
private ScheduledFuture scheduledFuture;
private static final long INTERNAL_JOB_CLEANUP_FREQUENCY = 1000L;

public AlternativeExecutorService()
{
    scheduledFuture = scheduledExecutor.scheduleAtFixedRate(new TimeoutManagerJob(), 0, INTERNAL_JOB_CLEANUP_FREQUENCY, TimeUnit.MILLISECONDS);
}

public void pushTask(OwnTask task)
{
    ListenableFuture<Void> future = threadExecutor.submit(task);  // -> create your Callable
    futureQueue.add(new ListenableFutureTask(future, task, getCurrentMillisecondsTime())); // -> store the time when the task should end
}

public void shutdownInternalScheduledExecutor()
{
    scheduledFuture.cancel(true);
    scheduledExecutor.shutdownNow();
}

long getCurrentMillisecondsTime()
{
    return Calendar.getInstance().get(Calendar.MILLISECOND);
}

class ListenableFutureTask
{
    private final ListenableFuture<Void> future;
    private final OwnTask                task;
    private final long                   milliSecEndTime;

    private ListenableFutureTask(ListenableFuture<Void> future, OwnTask task, long milliSecStartTime)
    {
        this.future = future;
        this.task = task;
        this.milliSecEndTime = milliSecStartTime + task.getTimeUnit().convert(task.getTimeoutDuration(), TimeUnit.MILLISECONDS);
    }

    ListenableFuture<Void> getFuture()
    {
        return future;
    }

    OwnTask getTask()
    {
        return task;
    }

    long getMilliSecEndTime()
    {
        return milliSecEndTime;
    }
}

class TimeoutManagerJob implements Runnable
{
    CopyOnWriteArrayList<ListenableFutureTask> getCopyOnWriteArrayList()
    {
        return futureQueue;
    }

    @Override
    public void run()
    {
        long currentMileSecValue = getCurrentMillisecondsTime();
        for (ListenableFutureTask futureTask : futureQueue)
        {
            consumeFuture(futureTask, currentMileSecValue);
        }
    }

    private void consumeFuture(ListenableFutureTask futureTask, long currentMileSecValue)
    {
        ListenableFuture<Void> future = futureTask.getFuture();
        boolean isTimeout = futureTask.getMilliSecEndTime() >= currentMileSecValue;
        if (isTimeout)
        {
            if (!future.isDone())
            {
                future.cancel(true);
            }
            futureQueue.remove(futureTask);
        }
    }
}

class OwnTask implements Callable<Void>
{
    private long     timeoutDuration;
    private TimeUnit timeUnit;

    OwnTask(long timeoutDuration, TimeUnit timeUnit)
    {
        this.timeoutDuration = timeoutDuration;
        this.timeUnit = timeUnit;
    }

    @Override
    public Void call() throws Exception
    {
        // do logic
        return null;
    }

    public long getTimeoutDuration()
    {
        return timeoutDuration;
    }

    public TimeUnit getTimeUnit()
    {
        return timeUnit;
    }
}
}
0
Ionut Mesaros