ExecutorService que interrumpe las tareas después de un tiempo de espera

93

Estoy buscando una implementación ExecutorService que se pueda proporcionar con un tiempo de espera. Las tareas que se envían al ExecutorService se interrumpen si tardan más en ejecutarse que el tiempo de espera. Implementar tal bestia no es una tarea tan difícil, pero me pregunto si alguien sabe de una implementación existente.

Esto es lo que se me ocurrió en base a parte de la discusión a continuación. ¿Algún comentario?

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();
        }
    }
}
Edward Dale
fuente
¿Es esa 'hora de inicio' del tiempo de espera la hora de envío? ¿O el momento en que la tarea comienza a ejecutarse?
Tim Bender
Buena pregunta. Cuando comienza a ejecutarse. Presumiblemente usando el protected void beforeExecute(Thread t, Runnable r)gancho.
Edward Dale
@ scompt.com ¿todavía está usando esta solución o ha sido reemplazada?
Paul Taylor
@PaulTaylor El trabajo donde implementé esta solución ha sido reemplazado. :-)
Edward Dale
Necesito exactamente esto, excepto a) Necesito que mi servicio de programador principal sea un grupo de subprocesos con un solo subproceso de servicio, ya que necesito que mis tareas se ejecuten estrictamente al mismo tiempo yb) Necesito poder especificar la duración del tiempo de espera para cada tarea en el hora en que se envía la tarea. He intentado usar esto como punto de partida, pero extendiendo ScheduledThreadPoolExecutor, pero no veo una forma de obtener la duración del tiempo de espera especificada que se especificará en el momento de envío de la tarea hasta el método beforeExecute. ¡Cualquier sugerencia agradecidamente apreciada!
Michael Ellis

Respuestas:

89

Puede utilizar un ScheduledExecutorService para esto. Primero, lo enviaría solo una vez para comenzar de inmediato y conservar el futuro que se crea. Después de eso, puede enviar una nueva tarea que cancelaría el futuro retenido después de un período de tiempo.

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

Esto ejecutará su controlador (la funcionalidad principal se interrumpirá) durante 10 segundos, luego cancelará (es decir, interrumpirá) esa tarea específica.

John Vint
fuente
13
Interesante idea, pero ¿y si la tarea finaliza antes del tiempo de espera (que normalmente lo hará)? Preferiría no tener toneladas de tareas de limpieza esperando para ejecutarse solo para descubrir que su tarea asignada ya se completó. Debería haber otro hilo monitoreando los Futuros a medida que terminan para eliminar sus tareas de limpieza.
Edward Dale
3
El albacea solo programará esta cancelación una vez. Si la tarea se completa, la cancelación es una operación no válida y el trabajo continúa sin cambios. Solo es necesario que haya un subproceso adicional para cancelar las tareas y un subproceso para ejecutarlas. Podría tener dos ejecutores, uno para enviar sus tareas principales y otro para cancelarlas.
John Vint
3
Eso es cierto, pero ¿qué pasa si el tiempo de espera es de 5 horas y en ese tiempo se ejecutan 10.000 tareas? Me gustaría evitar tener todas esas operaciones no operativas por ahí ocupando memoria y causando cambios de contexto.
Edward Dale
1
@Scompt No necesariamente. Habría 10k invocaciones de future.cancel (), sin embargo, si el futuro se completa, la cancelación se acelerará y no hará ningún trabajo innecesario. Si no desea 10.000 invocaciones de cancelación adicionales, es posible que esto no funcione, pero la cantidad de trabajo realizado cuando se completa una tarea es muy pequeña.
John Vint
6
@John W .: Me acabo de dar cuenta de otro problema con su implementación. Necesito que el tiempo de espera comience cuando la tarea comience a ejecutarse, como comenté anteriormente. Creo que la única forma de hacerlo es usando el beforeExecutegancho.
Edward Dale
6

Desafortunadamente, la solución es defectuosa. Hay una especie de error con ScheduledThreadPoolExecutor, también informado en esta pregunta : cancelar una tarea enviada no libera completamente los recursos de memoria asociados con la tarea; los recursos se liberan solo cuando la tarea expira.

Por lo tanto, si crea un TimeoutThreadPoolExecutorcon un tiempo de vencimiento bastante largo (un uso típico) y envía las tareas lo suficientemente rápido, terminará llenando la memoria, aunque las tareas se completaron con éxito.

Puede ver el problema con el siguiente programa de prueba (muy burdo):

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

El programa agota la memoria disponible, aunque espera Runnablea que se completen los mensajes de correo electrónico generados .

Pensé en esto por un tiempo, pero desafortunadamente no pude encontrar una buena solución.

EDITAR: descubrí que este problema se informó como error JDK 6602600 , y parece que se ha solucionado muy recientemente.

Flavio
fuente
4

Envuelva la tarea en FutureTask y puede especificar el tiempo de espera para FutureTask. Mira el ejemplo en mi respuesta a esta pregunta,

Tiempo de espera del proceso nativo de Java

Codificador ZZ
fuente
1
Me doy cuenta de que hay un par de formas de hacer esto usando las java.util.concurrentclases, pero estoy buscando una ExecutorServiceimplementación.
Edward Dale
1
Si está diciendo que desea que su ExecutorService oculte el hecho de que se están agregando tiempos de espera desde el código del cliente, puede implementar su propio ExecutorService que envuelve cada ejecutable que se le entrega con una FutureTask antes de ejecutarlos.
erikprice
2

Después de un montón de tiempo para estudiar,
finalmente, utilizo el invokeAllmétodo de ExecutorServicepara resolver este problema.
Eso interrumpirá estrictamente la tarea mientras se ejecuta la tarea.
Aquí hay un ejemplo

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

La ventaja es que también puede enviar ListenableFutureal mismo ExecutorService.
Simplemente cambie ligeramente la primera línea de código.

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

ListeningExecutorServicees la función de escucha del ExecutorServiceproyecto de google guava ( com.google.guava ))

Johnny
fuente
2
Gracias por señalar invokeAll. Eso funciona muy bien. Solo una advertencia para cualquiera que esté pensando en usar esto: aunque invokeAlldevuelve una lista de Futureobjetos, en realidad parece ser una operación de bloqueo.
mxro
1

Parece que el problema no está en el error JDK 6602600 (se resolvió el 22-05-2010), sino en una llamada incorrecta de suspensión (10) en círculo. Además, tenga en cuenta que el hilo principal debe dar directamente Oportunidad a otros hilos para realizar sus tareas invocando SLEEP (0) en CADA rama del círculo exterior. Creo que es mejor usar Thread.yield () en lugar de Thread.sleep (0)

La parte de resultado corregida del código de problema anterior es como esta:

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

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

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

Funciona correctamente con una cantidad de contador exterior hasta 150 000 000 de círculos probados.

Sergey
fuente
1

Usando la respuesta de John W, creé una implementación que comienza correctamente el tiempo de espera cuando la tarea comienza su ejecución. Incluso escribo una prueba unitaria para ello :)

Sin embargo, no se adapta a mis necesidades ya que algunas operaciones IO no interrumpen cuando Future.cancel()se llama (es decir, cuando Thread.interrupt()se llama). Algunos ejemplos de operaciones de IO que pueden no interrumpirse cuando Thread.interrupt()se llama son Socket.connecty Socket.read(y sospecho que la mayoría de las operaciones de IO se implementan en java.io). Todas las operaciones de E / S en java.niodeberían ser interrumpibles cuando Thread.interrupt()se llame. Por ejemplo, ese es el caso de SocketChannel.openy SocketChannel.read.

De todos modos, si alguien está interesado, creé una esencia para un ejecutor de grupo de subprocesos que permite que las tareas se agoten (si están usando operaciones interrumpibles ...): https://gist.github.com/amanteaux/64c54a913c1ae34ad7b86db109cbc0bf

amanteaux
fuente
Código interesante, lo introduje en mi sistema y tengo curiosidad por saber si tiene algunos ejemplos de qué tipo de operaciones de IO no se interrumpirán para que pueda ver si afectará a mi sistema. ¡Gracias!
Duncan Krebs
@DuncanKrebs Detallé mi respuesta con un ejemplo de IO no interrumpible: Socket.connectySocket.read
amanteaux
myThread.interrupted()no es el método correcto para interrumpir, ya que BORRA la bandera de interrupción. Úselo en su myThread.interrupt()lugar, y eso debería con sockets
DanielCuadra
@DanielCuadra: Gracias, parece que cometí un error tipográfico Thread.interrupted()que no permite interrumpir un hilo. Sin embargo, Thread.interrupt()no interrumpe las java.iooperaciones, solo java.niofunciona en operaciones.
amanteaux
Lo he usado interrupt()durante muchos años y siempre ha interrumpido las operaciones de java.io (así como otros métodos de bloqueo, como la suspensión de subprocesos, las conexiones jdbc, la toma de cola de bloqueo, etc.). Tal vez encontró una clase con errores o alguna JVM que tiene errores
DanielCuadra
0

¿Qué pasa con esta idea alternativa?

  • dos tienen dos albaceas:
    • uno para :
      • enviar la tarea, sin preocuparse por el tiempo de espera de la tarea
      • agregando el futuro resultante y el momento en que debería terminar a una estructura interna
    • uno para ejecutar un trabajo interno que es verificar la estructura interna si algunas tareas están en tiempo de espera y si deben cancelarse.

La pequeña muestra está aquí:

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;
    }
}
}
Ionut Mesaros
fuente
0

compruebe si esto funciona para usted,

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

puede restringir el número de usos de subprocesos del programador, así como poner tiempo de espera en la tarea.

Nitish Kumar
fuente