Transforme el futuro de Java en un futuro completo

97

Java 8 presenta CompletableFutureuna nueva implementación de Future que es componible (incluye un montón de métodos entoncesXxx). Me gustaría usar esto exclusivamente, pero muchas de las bibliotecas que quiero usar devuelven solo Futureinstancias no componibles .

¿Hay alguna manera de envolver las Futureinstancias devueltas dentro de una CompleteableFuturepara poder componerla?

Dan Midwood
fuente

Respuestas:

57

Hay una manera, pero no te gustará. El siguiente método transforma a Future<T>en a CompletableFuture<T>:

public static <T> CompletableFuture<T> makeCompletableFuture(Future<T> future) {
  if (future.isDone())
    return transformDoneFuture(future);
  return CompletableFuture.supplyAsync(() -> {
    try {
      if (!future.isDone())
        awaitFutureIsDoneInForkJoinPool(future);
      return future.get();
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    } catch (InterruptedException e) {
      // Normally, this should never happen inside ForkJoinPool
      Thread.currentThread().interrupt();
      // Add the following statement if the future doesn't have side effects
      // future.cancel(true);
      throw new RuntimeException(e);
    }
  });
}

private static <T> CompletableFuture<T> transformDoneFuture(Future<T> future) {
  CompletableFuture<T> cf = new CompletableFuture<>();
  T result;
  try {
    result = future.get();
  } catch (Throwable ex) {
    cf.completeExceptionally(ex);
    return cf;
  }
  cf.complete(result);
  return cf;
}

private static void awaitFutureIsDoneInForkJoinPool(Future<?> future)
    throws InterruptedException {
  ForkJoinPool.managedBlock(new ForkJoinPool.ManagedBlocker() {
    @Override public boolean block() throws InterruptedException {
      try {
        future.get();
      } catch (ExecutionException e) {
        throw new RuntimeException(e);
      }
      return true;
    }
    @Override public boolean isReleasable() {
      return future.isDone();
    }
  });
}

Obviamente, el problema con este enfoque es que para cada Futuro , se bloqueará un hilo para esperar el resultado del Futuro, contradiciendo la idea de futuros. En algunos casos, podría ser mejor hacerlo. Sin embargo, en general, no hay solución sin esperar activamente el resultado del Futuro .

nosid
fuente
1
Ja, eso es exactamente lo que escribí antes de pensar que debe haber una forma mejor. Pero supongo que no
Dan Midwood
12
Hmmm ... ¿esta solución no se come uno de los hilos del "fondo común", solo para esperar? Esos hilos de "piscina común" nunca deberían bloquearse ... hmmmm ...
Peti
1
@Peti: Tienes razón. Sin embargo, el punto es, si es más probable que esté haciendo algo mal, independientemente de si está utilizando el grupo común o un grupo de subprocesos ilimitado .
nosid
4
Puede que no sea perfecto, pero el uso CompletableFuture.supplyAsync(supplier, new SinglethreadExecutor())al menos no bloquearía los subprocesos comunes.
MikeFHay
6
Por favor, nunca hagas eso
Laymain
56

Si la biblioteca que desea utilizar también ofrece un método de estilo de devolución de llamada además del estilo Future, puede proporcionarle un controlador que complete CompletableFuture sin ningún bloqueo de subproceso adicional. Al igual que:

    AsynchronousFileChannel open = AsynchronousFileChannel.open(Paths.get("/some/file"));
    // ... 
    CompletableFuture<ByteBuffer> completableFuture = new CompletableFuture<ByteBuffer>();
    open.read(buffer, position, null, new CompletionHandler<Integer, Void>() {
        @Override
        public void completed(Integer result, Void attachment) {
            completableFuture.complete(buffer);
        }

        @Override
        public void failed(Throwable exc, Void attachment) {
            completableFuture.completeExceptionally(exc);
        }
    });
    completableFuture.thenApply(...)

Sin la devolución de llamada, la única otra forma que veo para resolver esto es usar un bucle de sondeo que coloca todas sus Future.isDone()comprobaciones en un solo hilo y luego invoca complete cada vez que se pueda obtener un Future.

Kafkiano
fuente
1
Estoy usando la biblioteca asíncrona Apache Http que acepta FutureCallback. Me hizo la vida más fácil :)
Abhishek Gayakwad
14

Si tu Futurees el resultado de una llamada a un ExecutorServicemétodo (por ejemplo submit()), lo más fácil sería usar el CompletableFuture.runAsync(Runnable, Executor)método en su lugar.

Desde

Runnbale myTask = ... ;
Future<?> future = myExecutor.submit(myTask);

a

Runnbale myTask = ... ;
CompletableFuture<?> future = CompletableFuture.runAsync(myTask, myExecutor);

A CompletableFuturecontinuación, se crea "de forma nativa".

EDITAR: Continuando con los comentarios de @SamMefford corregidos por @MartinAndersson, si desea pasar a Callable, debe llamar supplyAsync(), convirtiendo el Callable<T>en a Supplier<T>, por ejemplo, con:

CompletableFuture.supplyAsync(() -> {
    try { return myCallable.call(); }
    catch (Exception ex) { throw new RuntimeException(ex); } // Or return default value
}, myExecutor);

Debido a que T Callable.call() throws Exception;lanza una excepción y T Supplier.get();no lo hace, debe detectar la excepción para que los prototipos sean compatibles.

Matthieu
fuente
1
O, si está utilizando Callable <T> en lugar de Runnable, intente en su lugar supplyAsync:CompletableFuture<T> future = CompletableFuture.supplyAsync(myCallable, myExecutor);
Sam Mefford
@SamMefford gracias, edité para incluir esa información.
Matthieu
supplyAsyncrecibe un Supplier. El código no se compilará si intenta pasar un Callable.
Martin Andersson
@MartinAndersson es cierto, gracias. Edité más para convertir un Callable<T>en un Supplier<T>.
Matthieu
10

Publiqué un pequeño proyecto de futuro que intenta hacer algo mejor que el camino directo en la respuesta.

La idea principal es usar el único hilo (y, por supuesto, no solo con un bucle giratorio) para verificar todos los estados de Futures en el interior, lo que ayuda a evitar bloquear un hilo de un grupo para cada transformación Future -> CompletableFuture.

Ejemplo de uso:

Future oldFuture = ...;
CompletableFuture profit = Futurity.shift(oldFuture);
Dmitry Spikhalskiy
fuente
Esto parece interesante. ¿Está usando un hilo de temporizador? ¿Por qué esta no es la respuesta aceptada?
Kira
@Kira Sí, básicamente usa un hilo de temporizador para esperar todos los futuros enviados.
Dmitry Spikhalskiy
8

Sugerencia:

http://www.thedevpiece.com/converting-old-java-future-to-completablefuture/

Pero, básicamente:

public class CompletablePromiseContext {
    private static final ScheduledExecutorService SERVICE = Executors.newSingleThreadScheduledExecutor();

    public static void schedule(Runnable r) {
        SERVICE.schedule(r, 1, TimeUnit.MILLISECONDS);
    }
}

Y, la Promesa Completable:

public class CompletablePromise<V> extends CompletableFuture<V> {
    private Future<V> future;

    public CompletablePromise(Future<V> future) {
        this.future = future;
        CompletablePromiseContext.schedule(this::tryToComplete);
    }

    private void tryToComplete() {
        if (future.isDone()) {
            try {
                complete(future.get());
            } catch (InterruptedException e) {
                completeExceptionally(e);
            } catch (ExecutionException e) {
                completeExceptionally(e.getCause());
            }
            return;
        }

        if (future.isCancelled()) {
            cancel(true);
            return;
        }

        CompletablePromiseContext.schedule(this::tryToComplete);
    }
}

Ejemplo:

public class Main {
    public static void main(String[] args) {
        final ExecutorService service = Executors.newSingleThreadExecutor();
        final Future<String> stringFuture = service.submit(() -> "success");
        final CompletableFuture<String> completableFuture = new CompletablePromise<>(stringFuture);

        completableFuture.whenComplete((result, failure) -> {
            System.out.println(result);
        });
    }
}
Gabriel Francisco
fuente
esto es bastante simple de razonar y elegante y se adapta a la mayoría de los casos de uso. Haría lo CompletablePromiseContext no estático y tomaría el parámetro para el intervalo de verificación (que se establece en 1 ms aquí) y luego sobrecargaría el CompletablePromise<V>constructor para poder proporcionar el suyo CompletablePromiseContextcon un intervalo de verificación posiblemente diferente (más largo) para una ejecución prolongada Future<V>donde no No es necesario que pueda ejecutar la devolución de llamada (o redactar) inmediatamente después de terminar, y también puede tener una instancia de CompletablePromiseContextpara ver un conjunto de Future(en caso de que tenga muchos)
Dexter Legaspi
5

Permítanme sugerirles otra opción (con suerte, mejor): https://github.com/vsilaev/java-async-await/tree/master/com.farata.lang.async.examples/src/main/java/com/farata /concurrente

Brevemente, la idea es la siguiente:

  1. Presentar la CompletableTask<V>interfaz: la unión del CompletionStage<V>+RunnableFuture<V>
  2. Deformar ExecutorServicepara volver CompletableTaskde submit(...)métodos (en lugar de Future<V>)
  3. Hecho, tenemos Futuros ejecutables Y componibles.

La implementación utiliza una implementación alternativa de CompletionStage (preste atención, CompletionStage en lugar de CompletableFuture):

Uso:

J8ExecutorService exec = J8Executors.newCachedThreadPool();
CompletionStage<String> = exec
   .submit( someCallableA )
   .thenCombineAsync( exec.submit(someCallableB), (a, b) -> a + " " + b)
   .thenCombine( exec.submit(someCallableC), (ab, b) -> ab + " " + c); 
Valery Silaev
fuente
2
Pequeña actualización: el código se mueve a un proyecto separado, github.com/vsilaev/tascalate-concurrent , y ahora es posible usar Executor-s de caja fuera del camino de java.util.concurrent.
Valery Silaev