ExecutorService che interrompe le attività dopo un timeout


93

Sto cercando un'implementazione ExecutorService che può essere fornita con un timeout. Le attività inviate a ExecutorService vengono interrotte se richiedono più tempo del timeout per l'esecuzione. L'implementazione di una tale bestia non è un compito così difficile, ma mi chiedo se qualcuno sappia di un'implementazione esistente.

Ecco cosa mi è venuto in mente sulla base di alcune delle discussioni seguenti. Eventuali commenti?

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

Quella "ora di inizio" del timeout è l'ora della presentazione? O l'ora in cui inizia l'esecuzione dell'attività?
Tim Bender

Buona domanda. Quando inizia l'esecuzione. Presumibilmente usando il protected void beforeExecute(Thread t, Runnable r)gancio.
Edward Dale

@ scompt.com stai ancora utilizzando questa soluzione o è stata sostituita
Paul Taylor,

@PaulTaylor Il lavoro in cui ho implementato questa soluzione è stato sostituito. :-)
Edward Dale

Ho bisogno esattamente di questo, tranne a) Ho bisogno che il mio servizio di pianificazione principale sia un pool di thread con un singolo thread di servizio poiché è necessario che le mie attività vengano eseguite rigorosamente simultaneamente eb) Devo essere in grado di specificare la durata del timeout per ciascuna attività al l'ora in cui l'attività viene inviata. Ho provato a usarlo come punto di partenza ma estendendo ScheduledThreadPoolExecutor, ma non riesco a vedere un modo per ottenere la durata del timeout specificata che deve essere specificata al momento dell'invio dell'attività fino al metodo beforeExecute. Eventuali suggerimenti apprezzati con gratitudine!
Michael Ellis,

Risposte:


89

È possibile utilizzare ScheduledExecutorService per questo. Per prima cosa dovresti inviarlo solo una volta per iniziare immediatamente e conservare il futuro che si crea. Successivamente è possibile inviare una nuova attività che annullerebbe il futuro conservato dopo un certo periodo di tempo.

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

Questo eseguirà il tuo gestore (la funzionalità principale verrà interrotta) per 10 secondi, quindi annullerà (cioè interromperà) quella specifica attività.


12
Idea interessante, ma cosa succede se l'attività termina prima del timeout (cosa che normalmente farà)? Preferirei non avere tonnellate di attività di pulizia in attesa di essere eseguite solo per scoprire che l'attività assegnata è già stata completata. Ci dovrebbe essere un altro thread che monitora i Futures mentre finiscono per rimuovere le loro attività di pulizia.
Edward Dale

3
L'esecutore pianificherà questa cancellazione solo una volta. Se l'attività è completata, l'annullamento non è un'operazione e il lavoro continua invariato. Ci deve essere solo un thread in più che complotta per annullare le attività e un thread per eseguirle. Potresti avere due esecutori, uno per inviare le tue attività principali e uno per annullarle.
John Vint

3
È vero, ma cosa succede se il timeout è di 5 ore e in quel lasso di tempo vengono eseguite 10k attività. Vorrei evitare che tutti quei no-op in giro occupino memoria e causino cambi di contesto.
Edward Dale

1
@Scompt Non necessariamente. Ci sarebbero 10k invocazioni future.cancel (), tuttavia se il futuro è completato, l'annullamento verrà eliminato rapidamente e non farà alcun lavoro indesiderato. Se non vuoi 10k chiamate di annullamento extra, questo potrebbe non funzionare, ma la quantità di lavoro svolto quando un'attività è completata è molto piccola.
John Vint

6
@ John W .: Ho appena realizzato un altro problema con la tua implementazione. Ho bisogno che il timeout inizi quando l'attività inizia l'esecuzione, come ho commentato in precedenza. Penso che l'unico modo per farlo sia usare il beforeExecutegancio.
Edward Dale

6

Purtroppo la soluzione è viziata. C'è una sorta di bug con ScheduledThreadPoolExecutor, riportato anche in questa domanda : l'annullamento di un'attività inoltrata non rilascia completamente le risorse di memoria associate all'attività; le risorse vengono rilasciate solo quando l'attività scade.

Se quindi crei un TimeoutThreadPoolExecutorcon un tempo di scadenza abbastanza lungo (un utilizzo tipico) e invii le attività abbastanza velocemente, finisci per riempire la memoria, anche se le attività sono state effettivamente completate con successo.

Puoi vedere il problema con il seguente programma di test (molto rozzo):

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

Il programma esaurisce la memoria disponibile, anche se attende il Runnablecompletamento dei messaggi generati .

Ci ho pensato per un po ', ma sfortunatamente non sono riuscito a trovare una buona soluzione.

EDIT: ho scoperto che questo problema è stato segnalato come bug JDK 6602600 e sembra essere stato risolto molto di recente.


4

Avvolgi l'attività in FutureTask e puoi specificare il timeout per FutureTask. Guarda l'esempio nella mia risposta a questa domanda,

Timeout del processo nativo java


1
Mi rendo conto che ci sono un paio di modi per farlo usando le java.util.concurrentclassi, ma sto cercando ExecutorServiceun'implementazione.
Edward Dale

1
Se stai dicendo che vuoi che il tuo ExecutorService nasconda il fatto che i timeout vengono aggiunti dal codice client, potresti implementare il tuo ExecutorService che avvolge ogni eseguibile consegnato ad esso con un FutureTask prima di eseguirli.
erikprice

2

Dopo un sacco di tempo per esaminare,
infine, utilizzo il invokeAllmetodo ExecutorServiceper risolvere questo problema.
Ciò interromperà rigorosamente l'attività durante l'esecuzione dell'attività.
Ecco un esempio

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

Il vantaggio è che puoi anche inviare ListenableFutureallo stesso ExecutorService.
Cambia solo leggermente la prima riga di codice.

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

ListeningExecutorServiceè la funzione di ascolto del ExecutorServiceprogetto google guava ( com.google.guava ))


2
Grazie per avermelo fatto notare invokeAll. Funziona molto bene. Solo una parola di cautela per chiunque pensi di usarlo: sebbene invokeAllrestituisca un elenco di Futureoggetti, in realtà sembra essere un'operazione di blocco.
mxro


1

Sembra che il problema non sia nel bug JDK 6602600 (è stato risolto il 22/05/2010), ma nella chiamata errata del sonno (10) in cerchio. Nota in aggiunta, che il Thread principale deve dare direttamente la possibilità ad altri thread di realizzare i loro compiti invocando SLEEP (0) in OGNI ramo del cerchio esterno. È meglio, penso, usare Thread.yield () invece di Thread.sleep (0)

La parte corretta del risultato del precedente codice del problema è simile a questa:

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

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

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

Funziona correttamente con una quantità di contatore esterno fino a 150.000.000 di cerchi testati.


1

Utilizzando la risposta di John W ho creato un'implementazione che avvia correttamente il timeout quando l'attività inizia la sua esecuzione. Ho persino scritto un test unitario per questo :)

Tuttavia, non soddisfa le mie esigenze poiché alcune operazioni di I / O non si interrompono quando Future.cancel()viene chiamato (cioè quando Thread.interrupt()viene chiamato). Alcuni esempi di operazioni di I / O che potrebbero non essere interrotte quando Thread.interrupt()viene chiamato sono Socket.connecte Socket.read(e sospetto che la maggior parte delle operazioni di I / O implementate in java.io). Tutte le operazioni di I / O in java.niodovrebbero essere interrompibili quando Thread.interrupt()viene chiamato. Ad esempio, questo è il caso di SocketChannel.opene SocketChannel.read.

Ad ogni modo, se qualcuno è interessato, ho creato un gist per un esecutore di pool di thread che consente il timeout delle attività (se stanno utilizzando operazioni interrompibili ...): https://gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf


Codice interessante, l'ho inserito nel mio sistema e sono curioso di sapere se hai alcuni esempi di quale tipo di operazioni di I / O non si interromperanno, così posso vedere se avrà un impatto sul mio sistema. Grazie!
Duncan Krebs

@DuncanKrebs Ho dettagliato la mia risposta con un esempio di IO non interrompibile: Socket.connecteSocket.read
amanteaux

myThread.interrupted()non è il metodo corretto per interrompere, in quanto CANCELLA il flag di interruzione. Usa myThread.interrupt()invece, e dovrebbe con prese
DanielCuadra

@DanielCuadra: Grazie, sembra che ho fatto un errore di battitura in quanto Thread.interrupted()non consente di interrompere un thread. Tuttavia, Thread.interrupt()non interrompe le java.iooperazioni, funziona solo sulle java.niooperazioni.
amanteaux

L'ho usato interrupt()per molti anni e ha sempre interrotto le operazioni di java.io (così come altri metodi di blocco, come thread sleep, connessioni jdbc, blockingqueue take, ecc.). Forse hai trovato una classe con errori o qualche JVM che ha bug
DanielCuadra

0

Che dire di questa idea alternativa:

  • due hanno due esecutori:
    • uno per :
      • inviare l'attività, senza preoccuparsi del timeout dell'attività
      • aggiungendo il risultato futuro e il tempo in cui dovrebbe finire a una struttura interna
    • uno per eseguire un lavoro interno che controlla la struttura interna se alcune attività sono scadute e se devono essere annullate.

Un piccolo campione è qui:

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

controlla se questo funziona per te,

    public <T,S,K,V> ResponseObject<Collection<ResponseObject<T>>> runOnScheduler(ThreadPoolExecutor threadPoolExecutor,
      int parallelismLevel, TimeUnit timeUnit, int timeToCompleteEachTask, Collection<S> collection,
      Map<K,V> context, Task<T,S,K,V> someTask){
    if(threadPoolExecutor==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("threadPoolExecutor can not be null").build();
    }
    if(someTask==null){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("Task can not be null").build();
    }
    if(CollectionUtils.isEmpty(collection)){
      return ResponseObject.<Collection<ResponseObject<T>>>builder().errorCode("500").errorMessage("input collection can not be empty").build();
    }

    LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue = new LinkedBlockingQueue<>(collection.size());
    collection.forEach(value -> {
      callableLinkedBlockingQueue.offer(()->someTask.perform(value,context)); //pass some values in callable. which can be anything.
    });
    LinkedBlockingQueue<Future<T>> futures = new LinkedBlockingQueue<>();

    int count = 0;

    while(count<parallelismLevel && count < callableLinkedBlockingQueue.size()){
      Future<T> f = threadPoolExecutor.submit(callableLinkedBlockingQueue.poll());
      futures.offer(f);
      count++;
    }

    Collection<ResponseObject<T>> responseCollection = new ArrayList<>();

    while(futures.size()>0){
      Future<T> future = futures.poll();
      ResponseObject<T> responseObject = null;
        try {
          T response = future.get(timeToCompleteEachTask, timeUnit);
          responseObject = ResponseObject.<T>builder().data(response).build();
        } catch (InterruptedException e) {
          future.cancel(true);
        } catch (ExecutionException e) {
          future.cancel(true);
        } catch (TimeoutException e) {
          future.cancel(true);
        } finally {
          if (Objects.nonNull(responseObject)) {
            responseCollection.add(responseObject);
          }
          futures.remove(future);//remove this
          Callable<T> callable = getRemainingCallables(callableLinkedBlockingQueue);
          if(null!=callable){
            Future<T> f = threadPoolExecutor.submit(callable);
            futures.add(f);
          }
        }

    }
    return ResponseObject.<Collection<ResponseObject<T>>>builder().data(responseCollection).build();
  }

  private <T> Callable<T> getRemainingCallables(LinkedBlockingQueue<Callable<T>> callableLinkedBlockingQueue){
    if(callableLinkedBlockingQueue.size()>0){
      return callableLinkedBlockingQueue.poll();
    }
    return null;
  }

è possibile limitare il numero di utilizzi dei thread dallo scheduler e impostare il timeout per l'attività.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.