Esperando en una lista de Futuro


145

Tengo un método que devuelve un Listfuturo

List<Future<O>> futures = getFutures();

Ahora quiero esperar hasta que todos los futuros se terminen de procesar con éxito o cualquiera de las tareas cuyo resultado sea devuelto por un futuro arroje una excepción. Incluso si una tarea arroja una excepción, no tiene sentido esperar a los otros futuros.

Un enfoque simple sería

wait() {

   For(Future f : futures) {
     try {
       f.get();
     } catch(Exception e) {
       //TODO catch specific exception
       // this future threw exception , means somone could not do its task
       return;
     }
   }
}

Pero el problema aquí es si, por ejemplo, el 4to futuro arroja una excepción, entonces esperaré innecesariamente a que estén disponibles los primeros 3 futuros.

¿Cómo resolver esto? ¿La cuenta regresiva ayudará con el pestillo de alguna manera? No puedo usar Future isDoneporque el documento de Java dice

boolean isDone()
Returns true if this task completed. Completion may be due to normal termination, an exception, or cancellation -- in all of these cases, this method will return true.

1
¿Quién genera esos futuros? ¿De qué tipo son? La interfaz java.util.concurrent.Future no proporciona la funcionalidad que desea, la única forma es usar sus propios futuros con devoluciones de llamada.
Alexei Kaigorodov el

Podría hacer una instancia de ExecutionServicecada "lote" de tareas, enviarlas a él, luego inmediatamente cerrar el servicio y usarlo awaitTermination(), supongo.
millimoose

Podrías usar a CountDownLatchsi envolviste el cuerpo de todos tus futuros en una try..finallypara asegurarte de que el pestillo también se reduzca.
millimoose


@AlexeiKaigorodov SÍ, mi futuro es del tipo java.util.concurrent. Estoy demandando el futuro con invocable. Obtengo Futture cuando envío una tarea a un servicio de ejecución
user93796

Respuestas:


124

Puede usar un Servicio de finalización para recibir los futuros tan pronto como estén listos y si uno de ellos arroja una excepción, cancele el procesamiento. Algo como esto:

Executor executor = Executors.newFixedThreadPool(4);
CompletionService<SomeResult> completionService = 
       new ExecutorCompletionService<SomeResult>(executor);

//4 tasks
for(int i = 0; i < 4; i++) {
   completionService.submit(new Callable<SomeResult>() {
       public SomeResult call() {
           ...
           return result;
       }
   });
}

int received = 0;
boolean errors = false;

while(received < 4 && !errors) {
      Future<SomeResult> resultFuture = completionService.take(); //blocks if none available
      try {
         SomeResult result = resultFuture.get();
         received ++;
         ... // do something with the result
      }
      catch(Exception e) {
             //log
         errors = true;
      }
}

Creo que puede mejorar aún más para cancelar cualquier tarea que aún se esté ejecutando si uno de ellos arroja un error.


1
: Su código tiene el mismo problema que mencioné en mi publicación. o completeSerice.take) devolverá el futuro que se completa primero?
user93796

1
¿Qué pasa con los tiempos de espera? ¿Puedo decirle al servicio de finalización que espere X segundos como máximo?
user93796

1
No debería tener. No itera sobre los futuros, pero tan pronto como uno está listo, se procesa / verifica si no se produce una excepción.
dcernahoschi

2
Para agotar el tiempo de espera de que aparezca un futuro en la cola, hay un método de sondeo (segundos) en el CompletionService.
dcernahoschi

Aquí está el ejemplo de trabajo en github: github.com/princegoyal1987/FutureDemo
user18853

107

Si está utilizando Java 8 , puede hacerlo más fácilmente con CompletableFuture y CompletableFuture.allOf , que aplica la devolución de llamada solo después de completar todos los CompletableFutures suministrados.

// Waits for *all* futures to complete and returns a list of results.
// If *any* future completes exceptionally then the resulting future will also complete exceptionally.

public static <T> CompletableFuture<List<T>> all(List<CompletableFuture<T>> futures) {
    CompletableFuture[] cfs = futures.toArray(new CompletableFuture[futures.size()]);

    return CompletableFuture.allOf(cfs)
            .thenApply(ignored -> futures.stream()
                                    .map(CompletableFuture::join)
                                    .collect(Collectors.toList())
            );
}

3
Hola @Andrejs, ¿podrías explicar qué hace este fragmento de código? Veo esto sugerido en varios lugares, pero estoy confundido sobre lo que realmente está sucediendo. ¿Cómo se manejan las excepciones si falla uno de los hilos?
VSEWHGHP

2
@VSEWHGHP Desde el javadoc: Si alguno de los CompletableFutures dados se completa excepcionalmente, entonces el CompletableFuture devuelto también lo hace, con una CompletionException sosteniendo esta excepción como su causa.
Andrejs

1
Correcto, así que seguí con eso, ¿hay alguna forma de usar este fragmento pero obtener los valores para todos los otros hilos que se completaron con éxito? ¿Debo iterar sobre la lista CompletableFutures y llamar a get ignorando CompletableFuture <List <T>> ya que la función de secuencia se encarga de garantizar que todos los hilos estén completos, ya sea con resultado o excepción?
VSEWHGHP

66
Esto está resolviendo un problema diferente. Si tiene Futureinstancias, no puede aplicar este método. No es fácil de convertir Futureen CompletableFuture.
Jarekczek

no funcionará si tenemos una excepción en alguna tarea.
slisnychyi

21

Use a CompletableFutureen Java 8

    // Kick of multiple, asynchronous lookups
    CompletableFuture<User> page1 = gitHubLookupService.findUser("Test1");
    CompletableFuture<User> page2 = gitHubLookupService.findUser("Test2");
    CompletableFuture<User> page3 = gitHubLookupService.findUser("Test3");

    // Wait until they are all done
    CompletableFuture.allOf(page1,page2,page3).join();

    logger.info("--> " + page1.get());

1
Esta debería ser la respuesta aceptada. También es parte de la documentación oficial de primavera: spring.io/guides/gs/async-method
maaw

Funciona como se esperaba.
Dimon

15

Puede usar un ExecutorCompletionService . La documentación incluso tiene un ejemplo para su caso de uso exacto:

Supongamos, en cambio, que desea utilizar el primer resultado no nulo del conjunto de tareas, ignorando las que encuentren excepciones y cancelando todas las demás tareas cuando la primera esté lista:

void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException {
    CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
    int n = solvers.size();
    List<Future<Result>> futures = new ArrayList<Future<Result>>(n);
    Result result = null;
    try {
        for (Callable<Result> s : solvers)
            futures.add(ecs.submit(s));
        for (int i = 0; i < n; ++i) {
            try {
                Result r = ecs.take().get();
                if (r != null) {
                    result = r;
                    break;
                }
            } catch (ExecutionException ignore) {
            }
        }
    } finally {
        for (Future<Result> f : futures)
            f.cancel(true);
    }

    if (result != null)
        use(result);
}

Lo importante a tener en cuenta aquí es que ecs.take () obtendrá la primera tarea completada , no solo la primera enviada. Por lo tanto, debe obtenerlos en el orden de terminar la ejecución (o lanzar una excepción).


3

Si está utilizando Java 8 y no desea manipular CompletableFutures, he escrito una herramienta para recuperar resultados para una List<Future<T>>transmisión de uso. La clave es que tienes prohibido hacerlo map(Future::get)mientras se lanza.

public final class Futures
{

    private Futures()
    {}

    public static <E> Collector<Future<E>, Collection<E>, List<E>> present()
    {
        return new FutureCollector<>();
    }

    private static class FutureCollector<T> implements Collector<Future<T>, Collection<T>, List<T>>
    {
        private final List<Throwable> exceptions = new LinkedList<>();

        @Override
        public Supplier<Collection<T>> supplier()
        {
            return LinkedList::new;
        }

        @Override
        public BiConsumer<Collection<T>, Future<T>> accumulator()
        {
            return (r, f) -> {
                try
                {
                    r.add(f.get());
                }
                catch (InterruptedException e)
                {}
                catch (ExecutionException e)
                {
                    exceptions.add(e.getCause());
                }
            };
        }

        @Override
        public BinaryOperator<Collection<T>> combiner()
        {
            return (l1, l2) -> {
                l1.addAll(l2);
                return l1;
            };
        }

        @Override
        public Function<Collection<T>, List<T>> finisher()
        {
            return l -> {

                List<T> ret = new ArrayList<>(l);
                if (!exceptions.isEmpty())
                    throw new AggregateException(exceptions, ret);

                return ret;
            };

        }

        @Override
        public Set<java.util.stream.Collector.Characteristics> characteristics()
        {
            return java.util.Collections.emptySet();
        }
    }

Esto necesita un AggregateExceptionque funcione como C #

public class AggregateException extends RuntimeException
{
    /**
     *
     */
    private static final long serialVersionUID = -4477649337710077094L;

    private final List<Throwable> causes;
    private List<?> successfulElements;

    public AggregateException(List<Throwable> causes, List<?> l)
    {
        this.causes = causes;
        successfulElements = l;
    }

    public AggregateException(List<Throwable> causes)
    {
        this.causes = causes;
    }

    @Override
    public synchronized Throwable getCause()
    {
        return this;
    }

    public List<Throwable> getCauses()
    {
        return causes;
    }

    public List<?> getSuccessfulElements()
    {
        return successfulElements;
    }

    public void setSuccessfulElements(List<?> successfulElements)
    {
        this.successfulElements = successfulElements;
    }

}

Este componente actúa exactamente como la tarea de C # .WaitAll . Estoy trabajando en una variante que hace lo mismo que CompletableFuture.allOf(equivalente a Task.WhenAll)

La razón por la que hice esto es porque estoy usando Spring's ListenableFuture y no quiero portarlo a CompletableFuturepesar de que es una forma más estándar


1
Vota por ver la necesidad de una AggregateException equivalente.
granadaCoder

Un ejemplo de uso de esta instalación sería bueno.
XDS

1

En caso de que desee combinar una Lista de CompletableFutures, puede hacer esto:

List<CompletableFuture<Void>> futures = new ArrayList<>();
// ... Add futures to this ArrayList of CompletableFutures

// CompletableFuture.allOf() method demand a variadic arguments
// You can use this syntax to pass a List instead
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[futures.size()]));

// Wait for all individual CompletableFuture to complete
// All individual CompletableFutures are executed in parallel
allFutures.get();

Para obtener más detalles sobre Future & CompletableFuture, enlaces útiles:
1. Future: https://www.baeldung.com/java-future
2. CompletableFuture: https://www.baeldung.com/java-completablefuture
3. CompletableFuture: https : //www.callicoder.com/java-8-completablefuture-tutorial/


0

tal vez esto ayudaría (¡nada se reemplazaría con un hilo sin procesar, sí!) Sugiero ejecutar a cada Futurechico con un hilo separado (van paralelos), luego, cuando uno de los errores recibidos, solo indica al gerente ( Handlerclase).

class Handler{
//...
private Thread thisThread;
private boolean failed=false;
private Thread[] trds;
public void waitFor(){
  thisThread=Thread.currentThread();
  List<Future<Object>> futures = getFutures();
  trds=new Thread[futures.size()];
  for (int i = 0; i < trds.length; i++) {
    RunTask rt=new RunTask(futures.get(i), this);
    trds[i]=new Thread(rt);
  }
  synchronized (this) {
    for(Thread tx:trds){
      tx.start();
    }  
  }
  for(Thread tx:trds){
    try {tx.join();
    } catch (InterruptedException e) {
      System.out.println("Job failed!");break;
    }
  }if(!failed){System.out.println("Job Done");}
}

private List<Future<Object>> getFutures() {
  return null;
}

public synchronized void cancelOther(){if(failed){return;}
  failed=true;
  for(Thread tx:trds){
    tx.stop();//Deprecated but works here like a boss
  }thisThread.interrupt();
}
//...
}
class RunTask implements Runnable{
private Future f;private Handler h;
public RunTask(Future f,Handler h){this.f=f;this.h=h;}
public void run(){
try{
f.get();//beware about state of working, the stop() method throws ThreadDeath Error at any thread state (unless it blocked by some operation)
}catch(Exception e){System.out.println("Error, stopping other guys...");h.cancelOther();}
catch(Throwable t){System.out.println("Oops, some other guy has stopped working...");}
}
}

Tengo que decir que el código anterior sería un error (no se verificó), pero espero poder explicar la solución. por favor inténtalo


0
 /**
     * execute suppliers as future tasks then wait / join for getting results
     * @param functors a supplier(s) to execute
     * @return a list of results
     */
    private List getResultsInFuture(Supplier<?>... functors) {
        CompletableFuture[] futures = stream(functors)
                .map(CompletableFuture::supplyAsync)
                .collect(Collectors.toList())
                .toArray(new CompletableFuture[functors.length]);
        CompletableFuture.allOf(futures).join();
        return stream(futures).map(a-> {
            try {
                return a.get();
            } catch (InterruptedException | ExecutionException e) {
                //logger.error("an error occurred during runtime execution a function",e);
                return null;
            }
        }).collect(Collectors.toList());
    };

0

CompletionService tomará sus Callables con el método .submit () y puede recuperar los futuros calculados con el método .take ().

Una cosa que no debe olvidar es terminar el ExecutorService llamando al método .shutdown (). Además, solo puede llamar a este método cuando haya guardado una referencia al servicio del ejecutor, así que asegúrese de mantener una.

Código de ejemplo: para un número fijo de elementos de trabajo para trabajar en paralelo:

ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

CompletionService<YourCallableImplementor> completionService = 
new ExecutorCompletionService<YourCallableImplementor>(service);

ArrayList<Future<YourCallableImplementor>> futures = new ArrayList<Future<YourCallableImplementor>>();

for (String computeMe : elementsToCompute) {
    futures.add(completionService.submit(new YourCallableImplementor(computeMe)));
}
//now retrieve the futures after computation (auto wait for it)
int received = 0;

while(received < elementsToCompute.size()) {
 Future<YourCallableImplementor> resultFuture = completionService.take(); 
 YourCallableImplementor result = resultFuture.get();
 received ++;
}
//important: shutdown your ExecutorService
service.shutdown();

Código de ejemplo: para un número dinámico de elementos de trabajo para trabajar en paralelo:

public void runIt(){
    ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    CompletionService<CallableImplementor> completionService = new ExecutorCompletionService<CallableImplementor>(service);
    ArrayList<Future<CallableImplementor>> futures = new ArrayList<Future<CallableImplementor>>();

    //Initial workload is 8 threads
    for (int i = 0; i < 9; i++) {
        futures.add(completionService.submit(write.new CallableImplementor()));             
    }
    boolean finished = false;
    while (!finished) {
        try {
            Future<CallableImplementor> resultFuture;
            resultFuture = completionService.take();
            CallableImplementor result = resultFuture.get();
            finished = doSomethingWith(result.getResult());
            result.setResult(null);
            result = null;
            resultFuture = null;
            //After work package has been finished create new work package and add it to futures
            futures.add(completionService.submit(write.new CallableImplementor()));
        } catch (InterruptedException | ExecutionException e) {
            //handle interrupted and assert correct thread / work packet count              
        } 
    }

    //important: shutdown your ExecutorService
    service.shutdown();
}

public class CallableImplementor implements Callable{
    boolean result;

    @Override
    public CallableImplementor call() throws Exception {
        //business logic goes here
        return this;
    }

    public boolean getResult() {
        return result;
    }

    public void setResult(boolean result) {
        this.result = result;
    }
}

0

Tengo una clase de utilidad que contiene estos:

@FunctionalInterface
public interface CheckedSupplier<X> {
  X get() throws Throwable;
}

public static <X> Supplier<X> uncheckedSupplier(final CheckedSupplier<X> supplier) {
    return () -> {
        try {
            return supplier.get();
        } catch (final Throwable checkedException) {
            throw new IllegalStateException(checkedException);
        }
    };
}

Una vez que tenga eso, utilizando una importación estática, puede simplemente esperar todos los futuros como este:

futures.stream().forEach(future -> uncheckedSupplier(future::get).get());

También puede recopilar todos sus resultados de esta manera:

List<MyResultType> results = futures.stream()
    .map(future -> uncheckedSupplier(future::get).get())
    .collect(Collectors.toList());

Solo volví a visitar mi antigua publicación y me di cuenta de que tenías otro dolor:

Pero el problema aquí es si, por ejemplo, el 4to futuro arroja una excepción, entonces esperaré innecesariamente a que estén disponibles los primeros 3 futuros.

En este caso, la solución simple es hacer esto en paralelo:

futures.stream().parallel()
 .forEach(future -> uncheckedSupplier(future::get).get());

De esta manera, la primera excepción, aunque no detendrá el futuro, romperá la declaración forEach, como en el ejemplo en serie, pero como todos esperan en paralelo, no tendrá que esperar a que se completen los primeros 3.


0
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Stack2 {   
    public static void waitFor(List<Future<?>> futures) {
        List<Future<?>> futureCopies = new ArrayList<Future<?>>(futures);//contains features for which status has not been completed
        while (!futureCopies.isEmpty()) {//worst case :all task worked without exception, then this method should wait for all tasks
            Iterator<Future<?>> futureCopiesIterator = futureCopies.iterator();
            while (futureCopiesIterator.hasNext()) {
                Future<?> future = futureCopiesIterator.next();
                if (future.isDone()) {//already done
                    futureCopiesIterator.remove();
                    try {
                        future.get();// no longer waiting
                    } catch (InterruptedException e) {
                        //ignore
                        //only happen when current Thread interrupted
                    } catch (ExecutionException e) {
                        Throwable throwable = e.getCause();// real cause of exception
                        futureCopies.forEach(f -> f.cancel(true));//cancel other tasks that not completed
                        return;
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        Runnable runnable1 = new Runnable (){
            public void run(){
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
            }
        };
        Runnable runnable2 = new Runnable (){
            public void run(){
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                }
            }
        };


        Runnable fail = new Runnable (){
            public void run(){
                try {
                    Thread.sleep(1000);
                    throw new RuntimeException("bla bla bla");
                } catch (InterruptedException e) {
                }
            }
        };

        List<Future<?>> futures = Stream.of(runnable1,fail,runnable2)
                .map(executorService::submit)
                .collect(Collectors.toList());

        double start = System.nanoTime();
        waitFor(futures);
        double end = (System.nanoTime()-start)/1e9;
        System.out.println(end +" seconds");

    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.