เปลี่ยน Java Future ให้เป็น CompletableFuture


97

Java 8 แนะนำCompletableFutureการใช้งาน Future ใหม่ที่สามารถใช้ร่วมกันได้ (รวมถึงวิธีการ Xxx จำนวนมาก) ฉันต้องการใช้สิ่งนี้โดยเฉพาะ แต่หลาย ๆ ไลบรารีที่ฉันต้องการใช้จะส่งคืนเฉพาะFutureอินสแตนซ์ที่ไม่สามารถประกอบได้

มีวิธีการรวมFutureอินสแตนซ์ที่ส่งคืนภายใน a CompleteableFutureเพื่อที่ฉันจะเขียนมันได้หรือไม่

คำตอบ:


57

มีวิธี แต่คุณจะไม่ชอบ วิธีการต่อไปนี้จะแปลง a Future<T>เป็น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();
    }
  });
}

เห็นได้ชัดว่าปัญหาของแนวทางนี้คือในแต่ละอนาคตเธรดจะถูกปิดกั้นเพื่อรอผลของอนาคต - ซึ่งขัดแย้งกับแนวคิดเรื่องอนาคต ในบางกรณีอาจทำได้ดีกว่านี้ แต่โดยทั่วไปมีวิธีการแก้ปัญหาโดยไม่ต้องแข็งขันรอผลของการไม่มีอนาคต


1
ฮาตรงที่เขียนไว้ก่อนคิดว่าต้องมีวิธีที่ดีกว่านี้ แต่ฉันเดาว่าไม่ใช่
Dan Midwood

12
อืม ... วิธีแก้ปัญหานี้ไม่ได้กินหนึ่งในหัวข้อของ "สระว่ายน้ำทั่วไป" เพียงเพื่อรอ? เธรด "สระว่ายน้ำทั่วไป" เหล่านั้นไม่ควรปิดกั้น ...
อืม

1
@Peti: คุณพูดถูก อย่างไรก็ตามประเด็นก็คือหากคุณมักจะทำอะไรผิดพลาดไม่ว่าคุณจะใช้พูลทั่วไปหรือเธรดพูลที่ไม่ถูกผูกไว้
nosid

4
อาจไม่สมบูรณ์แบบ แต่CompletableFuture.supplyAsync(supplier, new SinglethreadExecutor())อย่างน้อยการใช้จะไม่บล็อกเธรดพูลทั่วไป
MikeFHay

6
ได้โปรดอย่าทำอย่างนั้น
Laymain

56

หากไลบรารีที่คุณต้องการใช้ยังมีวิธีการโทรกลับนอกเหนือจากสไตล์ในอนาคตคุณสามารถจัดเตรียมตัวจัดการที่ทำให้เสร็จสมบูรณ์ CompletableFuture โดยไม่มีการปิดกั้นเธรดเพิ่มเติม ชอบมาก:

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

หากไม่มีการโทรกลับวิธีเดียวที่ฉันเห็นการแก้ปัญหานี้คือการใช้ลูปการสำรวจที่ทำให้การFuture.isDone()ตรวจสอบทั้งหมดของคุณอยู่ในเธรดเดียวจากนั้นเรียกใช้การดำเนินการเมื่อใดก็ตามที่อนาคตจะได้รับ


1
ฉันใช้ Apache Http async library ซึ่งยอมรับ FutureCallback มันทำให้ชีวิตของฉันง่ายขึ้น :)
Abhishek Gayakwad

14

หากคุณFutureเป็นผลมาจากการเรียกใช้ExecutorServiceเมธอด (เช่นsubmit()) วิธีที่ง่ายที่สุดคือใช้CompletableFuture.runAsync(Runnable, Executor)วิธีนี้แทน

จาก

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

ถึง

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

CompletableFutureถูกสร้างขึ้นแล้ว "กำเนิด"

แก้ไข: การติดตามความคิดเห็นโดย @SamMefford แก้ไขโดย @MartinAndersson หากคุณต้องการส่งผ่านCallableคุณต้องโทรsupplyAsync()แปลงCallable<T>เป็น a Supplier<T>เช่น:

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

เนื่องจากT Callable.call() throws Exception;มีข้อยกเว้นและT Supplier.get();ไม่ทำคุณจึงต้องจับข้อยกเว้นเพื่อให้ต้นแบบเข้ากันได้


1
หรือถ้าคุณใช้ Callable <T> แทน Runnable ให้ลอง supplyAsync แทน:CompletableFuture<T> future = CompletableFuture.supplyAsync(myCallable, myExecutor);
Sam Mefford

@SamMefford ขอบคุณฉันแก้ไขเพื่อรวมข้อมูลนั้น
Matthieu

supplyAsyncได้รับSupplier. รหัสจะไม่คอมไพล์หากคุณพยายามส่งไฟล์Callable.
Martin Andersson

@MartinAndersson ถูกต้องขอบคุณ ฉันแก้ไขเพิ่มเติมเพื่อแปลงไฟล์Callable<T>เป็นSupplier<T>ไฟล์.
Matthieu

10

ฉันเผยแพร่โครงการไร้อนาคตเล็กน้อยที่พยายามทำให้ดีกว่าวิธีที่ตรงไปตรงมาในคำตอบ

แนวคิดหลักคือการใช้เธรดเดียว (และแน่นอนว่าไม่ใช่แค่การหมุนวน) เพื่อตรวจสอบสถานะฟิวเจอร์สทั้งหมดภายในซึ่งจะช่วยหลีกเลี่ยงการบล็อกเธรดจากพูลสำหรับการแปลงอนาคต -> เสร็จสมบูรณ์ในอนาคต

ตัวอย่างการใช้งาน:

Future oldFuture = ...;
CompletableFuture profit = Futurity.shift(oldFuture);

นี่ดูน่าสนใจ ใช้ด้ายจับเวลาหรือเปล่า นี่ไม่ใช่คำตอบที่ยอมรับได้อย่างไร
Kira

@ คิระใช่โดยทั่วไปจะใช้เธรดตัวจับเวลาเดียวเพื่อรอฟิวเจอร์สที่ส่งมาทั้งหมด
Dmitry Spikhalskiy

8

ข้อเสนอแนะ:

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

แต่โดยพื้นฐาน:

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

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

และ CompletablePromise:

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

ตัวอย่าง:

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

นี่เป็นเหตุผลที่ค่อนข้างง่ายและหรูหราและเหมาะกับกรณีการใช้งานส่วนใหญ่ ฉันจะทำให้CompletablePromiseContext ไม่คงที่และใช้พารามิเตอร์สำหรับช่วงเวลาการตรวจสอบ (ซึ่งถูกกำหนดเป็น 1 มิลลิวินาทีที่นี่) แล้วเกินCompletablePromise<V>ตัวสร้างเพื่อให้สามารถที่จะให้ตัวคุณเองCompletablePromiseContextด้วยการที่แตกต่างกันอาจจะเป็น (อีกต่อไป) ช่วงเวลาการตรวจสอบยาวทำงานFuture<V>ที่คุณ don ไม่จำเป็นต้องสามารถเรียกใช้การโทรกลับ (หรือเขียน) ได้ทันทีเมื่อเสร็จสิ้นและคุณยังสามารถCompletablePromiseContextดูชุดของFuture(ในกรณีที่คุณมีจำนวนมาก)
Dexter Legaspi

5

ให้ฉันแนะนำตัวเลือกอื่น (หวังว่าจะดีกว่า): https://github.com/vsilaev/java-async-await/tree/master/com.farata.lang.async.examples/src/main/java/com/farata / พร้อมกัน

โดยสังเขปแนวคิดดังต่อไปนี้:

  1. แนะนำCompletableTask<V>อินเทอร์เฟซ - การรวมกันของ CompletionStage<V>+RunnableFuture<V>
  2. วาร์ปExecutorServiceเพื่อกลับCompletableTaskจากsubmit(...)วิธีการ (แทนที่จะเป็นFuture<V>)
  3. เสร็จสิ้นเรามีฟิวเจอร์สที่รันได้และแบบประกอบได้

Implementation ใช้การใช้งาน CompletionStage ทางเลือก (ให้ความสนใจCompletionStageแทนที่จะเป็น CompletableFuture)

การใช้งาน:

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

2
การอัปเดตขนาดเล็ก: โค้ดถูกย้ายไปยังโปรเจ็กต์แยกgithub.com/vsilaev/tascalate-concurrentและตอนนี้คุณสามารถใช้ Executor-s จาก java.util.concurrent
Valery Silaev
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.