5 Ejecutar un runnable asíncronamente¶
Cuando se complete un CompletableFuture¶
El método thenRun(runnable)
nos permite encadenar la ejecución de determinada tarea, correspondiente al runnable recibido, cuando termine de ejecutarse la tarea representada por un CompletableFuture
, y se ejecutará en el mismo hilo que la tarea previa.
La segunda tarea NO recibirá el resultado de la tarea anterior, ya que al tratarse de un runnable su método run()
no recibe nada, por lo que simplemente se trata de una tarea terminal independiente.
Un aspecto importante es que cuando encadenamos tareas con CompletableFuture
, cada operación siempre produce un nuevo CompletableFuture
, no se modifica el anterior.
Veamos un ejemplo:
private void thenRunExample() {
CompletableFuture<Void> cf =
CompletableFuture.runAsync(this::printInfo)
.thenRun(this::printMoreInfo);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cf.join();
}
private void printInfo() {
System.out.printf("%s - Runnable1\n", Thread.currentThread().getName());
}
private void printMoreInfo() {
System.out.printf("%s - Runnable2\n", Thread.currentThread().getName());
}
Este método retorna un CompletableFuture<Void>
, ya que el runnable no retorna nada.
Debemos tener en cuenta que, bajo ciertas circunstancias, la segunda tarea puede estar ejecutándose en el hilo principal. En el ejemplo anterior, al encadenarlo con CompletableFuture.runAsync()
, la segunda tarea se ejecutará en el mismo hilo en el que se ejecutó la primera (que en este caso será del common fork-join pool). Sin embargo, simplemente con usar una estructura diferente en nuestro código puede darse el caso de que la segunda tarea se ejecute en el hilo principal:
private void thenRun2Example() {
CompletableFuture<Void> cf = CompletableFuture.runAsync(this::printInfo);
sleep(1000);
cf.thenRun(this::printMoreInfo);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
}
private void printInfo() {
System.out.printf("%s - Task1\n", Thread.currentThread().getName());
}
private void printMoreInfo() {
System.out.printf("%s - Task2\n", Thread.currentThread().getName());
}
private boolean sleep(long timeInMilis) {
try {
Thread.sleep(timeInMilis);
return true;
} catch (InterruptedException e) {
return false;
}
}
Warning
Por este motivo, debemos ser cautos a los hora de usar métodos de encadenamiento que no lleven el sufijo Async.
En este caso, existe un método alternativo, llamado thenRunAsync(runnable)
, similar a thenRun(runnable)
, pero en el que el runnable encadenada no tiene por qué ejecutarse en el mismo hilo que la tarea previa, sino que se elige otro hilo de entre los disponibles en el ejecutor por defecto.
private void thenRunAsyncExample() {
CompletableFuture<Void> cf = CompletableFuture.runAsync(this::printInfo);
sleep(1000);
cf.thenRunAsync(this::printMoreInfo);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
}
private void printInfo() {
System.out.printf("%s - Task1\n", Thread.currentThread().getName());
}
private void printMoreInfo() {
System.out.printf("%s - Task2\n", Thread.currentThread().getName());
}
private boolean sleep(long timeInMilis) {
try {
Thread.sleep(timeInMilis);
return true;
} catch (InterruptedException e) {
return false;
}
}
Al ejecutar este ejemplo veremos que la segunda tarea puede que se ejecute o no en el mismo hilo que la primera tarea.
Este método está sobrecargado thenRunAsync(runnable, executor)
para poder especificar el ejecutor en que queremos que se ejecute la tarea encadenada.
Cuando se complete el primero de dos CompletableFuture¶
En algunas ocasiones lanzamos dos cadenas de operaciones asíncronas, pero sólo estamos interesados en ejecutar una determinada acción cuando se complete la primera de las dos en hacerlo, independiente del valor con el que se haya completado.
Para este cometido tenemos disponible el método runAfterEitherAsync(completableFuture, runnable)
, que ejecuta el runnable pasado como argumento en cuanto uno de los dos CompletableFuture (sobre que el se ha ejecuta el método y el que se ha pasado como argumento) sea completado.
Este método retorna un CompletableFuture<Void>
, ya que el runnable no retorna nada, por lo que no se podrá encadenar más operaciones tras él. Por este motivo solo se puede usar este método al final de una cadena de operaciones.
El método se encuentra sobrecargado runAfterEitherAsync(completableFuture, runnable, executor)
, para recibir el executor en el que queremos ejecutar el runnable.
Un aspecto muy importante es que este método no cancela la ejecución de la segunda operación una vez que es completada la primera en ser completada.
Método runAfterEither
También existe el método runAfterEither(completableFuture, runnable)
, pero debemos tener en cuenta que bajo ciertas circunstancias es posible que el consumer se ejecute en el hilo principal, como vimos anteriormente en el caso de thenRun(runnable)
.
private void runAfterEitherAsyncExample() {
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(this::generateNumber1);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(this::generateNumber2);
CompletableFuture<Void> cfAfterFirst = cf1.runAfterEitherAsync(cf2, this::printInfo);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cfAfterFirst.join();
cf1.join();
cf2.join();
}
private int generateNumber1() {
int value = ThreadLocalRandom.current().nextInt(5) + 1;
sleep(value * 1000);
System.out.printf("%s - Supplier1 - %d\n", Thread.currentThread().getName(), value);
return value;
}
private int generateNumber2() {
int value = ThreadLocalRandom.current().nextInt(5) + 1;
sleep(value * 1000);
System.out.printf("%s - Supplier2 - %d\n", Thread.currentThread().getName(), value);
return value;
}
private void printInfo() {
System.out.printf("%s - Task\n", Thread.currentThread().getName());
}
private boolean sleep(long timeInMilis) {
try {
Thread.sleep(timeInMilis);
return true;
} catch (InterruptedException e) {
return false;
}
}
Cuando se completen dos CompletableFuture¶
En algunas ocasiones lanzamos dos cadenas de operaciones asíncronas y estamos interesados en ejecutar una determinada acción cuando se completen las dos operaciones, independiente de los valores con el que se hayan completado.
Para este cometido tenemos disponible el método runAfterBothAsync(completableFuture, runnable)
, que ejecuta el runnable pasado como argumento cuando los dos CompletableFuture (sobre que el se ha ejecuta el método y el que se ha pasado como argumento) son completados.
Este método retorna un CompletableFuture<Void>
, ya que el runnable no retorna nada, por lo que no se podrá encadenar más operaciones tras él. Por este motivo solo se puede usar este método al final de una cadena de operaciones.
El método se encuentra sobrecargado runAfterBothAsync(completableFuture, runnable, executor)
, para recibir el executor en el que queremos ejecutar el runnable.
Método runAfterBoth
También existe el método runAfterBoth(completableFuture, runnable)
, pero debemos tener en cuenta que bajo ciertas circunstancias es posible que el consumer se ejecute en el hilo principal, como vimos anteriormente en el caso de thenRun(runnable)
.
private void runAfterBothAsyncExample() {
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(this::generateNumber1);
CompletableFuture<Integer> cf2 = CompletableFuture.supplyAsync(this::generateNumber2);
CompletableFuture<Void> cfAfterBoth = cf1.runAfterBothAsync(cf2, this::printInfo);
System.out.printf("%s - Main\n", Thread.currentThread().getName());
cfAfterBoth.join();
}
private int generateNumber1() {
int value = ThreadLocalRandom.current().nextInt(5) + 1;
sleep(value * 1000);
System.out.printf("%s - Supplier1 - %d\n", Thread.currentThread().getName(), value);
return value;
}
private int generateNumber2() {
int value = ThreadLocalRandom.current().nextInt(5) + 1;
sleep(value * 1000);
System.out.printf("%s - Supplier2 - %d\n", Thread.currentThread().getName(), value);
return value;
}
private void printInfo() {
System.out.printf("%s - Task\n", Thread.currentThread().getName());
}
private boolean sleep(long timeInMilis) {
try {
Thread.sleep(timeInMilis);
return true;
} catch (InterruptedException e) {
return false;
}
}