2009-11-27 18 views
5

He extendido FutureTask de java.util.concurrent para proporcionar devoluciones de llamada para rastrear la ejecución de las tareas enviadas a ExecutorService.Extendiendo FutureTask, cómo manejar cancelar

public class StatusTask<V> extends FutureTask<V> { 

    private final ITaskStatusHandler<V> statusHandler; 

    public StatusTask(Callable<V> callable, ITaskStatusHandler<V> statusHandler){ 
     super(callable); 
     if (statusHandler == null) 
      throw new NullPointerException("statusHandler cannot be null"); 
     this.statusHandler = statusHandler; 
     statusHandler.TaskCreated(this); 
    } 

    @Override 
    public void run() { 
     statusHandler.TaskRunning(this); 
     super.run(); 
    } 

    @Override 
    protected void done() { 
     super.done(); 
     statusHandler.TaskCompleted(this); 
    } 

} 

Ahora, lo que veo es que si se presenta la tarea, pero termina en cola y me cancel(true); la tarea - el método run() todavía se llama - y la FutureTask.run() (probable) comprueba que la tarea se cancela y doesn Llamar al envolvente llamable.

¿Debo hacerlo, p.

@Override 
public void run() { 
    if(!isCancelled()) { 
    statusHandler.TaskRunning(this); 
    super.run(); 
    } 
} 

¿O todavía debo llamar al super.run()? Ambos enfoques parecen susceptibles a las condiciones de carrera entre verificar la cancelación y hacer algo al respecto ... cualquier idea apreciada.

Respuesta

3

Tienes razón que hay una carrera allí. FutureTask#done() se llamará a lo sumo, por lo que si la tarea ya se ha cancelado antes de ejecutarse alguna vez a través de RunnableFuture#run(), se perderá la llamada al FutureTask#done().

¿Ha considerado un enfoque más simple que siempre emite un conjunto simétrico de llamadas emparejadas a ITaskStatusHandler#taskRunning() y ITaskStatusHandler#taskCompleted(), como tal?

@Override 
public void run() { 
    statusHandler.TaskRunning(this); 
    try { 
    super.run(); 
    finally { 
    statusHandler.TaskCompleted(this); 
    } 
} 

Una vez RunnableFuture#run() se llama, es cierto que su tarea en ejecución, o al menos tratar de ser ejecutado. Una vez hecho el FutureTask#run(), su tarea ya no se está ejecutando. Sucede que en el caso de la cancelación, la transición es (casi) inmediata.

Tratando de evitar llamar ITaskStatusHandler#taskRunning() si el interno o CallableRunnable nunca es invocado por FutureTask#run() le requerirá para establecer algún tipo de estructura compartida entre el Callable o Runnable y el propio FutureTask tipo derivada de, por lo que cuando su función interna se llama primero establece un indicador que el tipo externo FutureTask -derived puede observar como un pestillo, lo que indica que sí, la función comenzó a ejecutarse antes de que se haya cancelado. Sin embargo, para entonces, tenía que haberse comprometido a llamar al ITaskStatusHandler#taskRunning(), por lo que la distinción no es tan útil.

He estado luchando con un problema de diseño similar últimamente, y terminó de decidirse por la simétrica antes y después de las operaciones en mi FutureTask#run() método anulado.

+0

Supongo que la llamada en la cláusula finally debe ser statusHandler.TaskCompleted (this); ¿Hay algún caso en el que no se invoque el método de ejecución, pero el método done() lo haría? – nos

+0

Gracias por tomar el error. Solucioné la llamada en el bloque finally. Sí, es posible que done() se pueda invocar sin ejecutar run(). Vea FutureTask # SynC# innerCancel (boolean). Allí, siempre que la tarea no haya terminado de ejecutarse, lo que incluye que nunca ha comenzado a ejecutarse, puede ver que se llamará a done(). Tenga en cuenta que done() se llamará cero o una vez para cualquier instancia de FutureTask: cero si nunca se llama ni a run() ni a cancel(), de lo contrario. – seh

+0

Parece que, si lo envía a un ejecutor, se llamará a done() si la tarea se cancela antes de que se ejecute, aunque el ejecutor no lo sabe. El ejecutor solo sabe sobre runnables/callables. Entonces, se llamará a run() cuando eventualmente se vuelva ejecutable, en ese caso FutureTask # run() esencialmente no hace nada. – nos

2

Su problema es que sus tareas futuras se siguen ejecutando después de haber llamado cancelar en ellas, ¿verdad?

Después de que una tarea se envió al servicio del ejecutor, debe ser administrada por el ejecutor. (Aún puede cancelar una única tarea si lo desea). Debe cancelar la ejecución con el executor shutdownNow method. (Esto llamará al método cancelar de todas las tareas enviadas). Un cierre aún ejecutará todas las tareas enviadas.

El ejecutor de otra forma no "sabe" que una tarea fue cancelada. Llamará al método independientemente del estado interno de la tarea futura.

El enfoque más fácil sería usar el marco Executor tal como está y escribir un decorador invocable.

class CallableDecorator{ 

    CallableDecorator(Decorated decorated){ 
    ... 
    } 

    setTask(FutureTask task){ 
    statusHandler.taskCreated(task); 
    } 

    void call(){ 
    try{ 
     statusHandler.taskRunning(task); 
     decorated.call(); 
    }finally{ 
     statusHandler.taskCompleted(task); 
    } 
    } 
} 

El único problema es que la tarea no puede estar en el constructor del decorador. (Es un parámetro del futuro constructor de tareas.) Para romper este ciclo, debe usar un setter o algún proxy para trabajar con la inyección del constructor. Tal vez esto no sea necesario para la devolución de llamada y usted puede decir: statusHandler.callableStarted(decorated).

Dependiendo de sus requisitos, puede que tenga que señalar excepciones e interrupciones.

aplicación básica:

class CallableDecorator<T> implements Callable<T> { 

    private final Callable<T> decorated; 
    CallableDecorator(Callable<T> decorated){ 
     this.decorated = decorated; 
    } 

    @Override public T call() throws Exception { 
     out.println("before " + currentThread()); 
     try { 
      return decorated.call(); 
     }catch(InterruptedException e){ 
      out.println("interupted " + currentThread()); 
      throw e; 
     } 
     finally { 
      out.println("after " + currentThread()); 
     } 
    } 
} 

ExecutorService executor = newFixedThreadPool(1); 
Future<Long> normal = executor.submit(new CallableDecorator<Long>(
     new Callable<Long>() { 
      @Override 
      public Long call() throws Exception { 
       return System.currentTimeMillis(); 
      } 
     })); 
out.println(normal.get()); 

Future<Long> blocking = executor.submit(new CallableDecorator<Long>(
     new Callable<Long>() { 
      @Override 
      public Long call() throws Exception { 
       sleep(MINUTES.toMillis(2)); // blocking call 
       return null; 
      } 
     })); 

sleep(SECONDS.toMillis(1)); 
blocking.cancel(true); // or executor.shutdownNow(); 

Salida:

before Thread[pool-1-thread-1,5,main] 
after Thread[pool-1-thread-1,5,main] 
1259347519104 
before Thread[pool-1-thread-1,5,main] 
interupted Thread[pool-1-thread-1,5,main] 
after Thread[pool-1-thread-1,5,main] 
+2

en serio, ¿quiere controlar a un individuo invocable matando a todo el ejecutor? eso funcionaría muy bien si tuvieras un ejecutor por invocable. en cuyo caso, ¿por qué usar ejecutores? por lo general, desea cancelar trabajos selectivamente, de ahí el método cancelar en Futuro. – james

+0

No. El código de muestra muestra cómo matar al ejecutor o una sola tarea. –