Java中使用CompletableFuture處理非同步超時
一天,我在改進多執行緒程式碼時被Future.get()卡住了。
public void serve() throws InterruptedException, ExecutionException, TimeoutException { final Future<Response> responseFuture = asyncCode(); final Response response = responseFuture.get(1, SECONDS); send(response); } private void send(Response response) { //... }
這是用Java寫的一個Akka應用程式,使用了一個包含1000個執行緒的執行緒池(原來如此!)——所有的執行緒都在阻塞在這個 get() 中。系統的處理速度跟不上併發請求的數量。重構以後,我們幹掉了所有的這些執行緒僅保留了一個,極大的減少了記憶體的佔用。我們簡單一點,通過一個Java 8的例子來演示。第一步是使用CompletableFuture來替換簡單的Future(見:Tip 9)。
- 通過控制任務提交到ExecutorService的方式:只需用 CompletableFuture.supplyAsync(…, executorService) 來代替 executorService.submit(…) 即可
- 處理基於回撥函式的API:使用promises
否則(如果你已經使用了阻塞式的API或 Future<T>)會導致很多執行緒被阻塞。這就是為什麼現在這麼多非同步的API都讓人很煩了。所以,讓我們重寫之前的程式碼來接收CompletableFuture:
public void serve() throws InterruptedException, ExecutionException, TimeoutException { final CompletableFuture<Response> responseFuture = asyncCode(); final Response response = responseFuture.get(1, SECONDS); send(response); }
很明顯,這不能解決任何問題,我們還必須利用新的風格來程式設計:
public void serve() { final CompletableFuture<Response> responseFuture = asyncCode(); responseFuture.thenAccept(this::send); }
這個功能上是等同的,但是 serve() 只會執行一小段時間(不會阻塞或等待)。只需要記住:this::send 將會在完成 responseFuture 的同一個執行緒內執行。如果你不想花費太大的代價來過載已經存在的執行緒池或send()方法,可以考慮通過 thenAcceptAsync(this::send, sendPool) 好極了,但是我們失去了兩個重要屬性:異常傳播與超時。異常傳播很難實現,因為我們改變了API。當serve()存在的時候,非同步操作可能還沒有完成。如果你關心異常,可以考慮返回 responseFutureor 或者其他可選的機制。至少,應該有異常的日誌,否則該異常就會被吞噬了。
final CompletableFuture<Response> responseFuture = asyncCode(); responseFuture.exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null; });
請小心上面的程式碼:exceptionally() 試圖從失敗中恢復過來,返回一個可選的結果。這個地方雖可以正常的工作,但是如果對 exceptionally()和withthenAccept() 使用鏈式呼叫,即使失敗了也還是會呼叫 send() 方法,返回一個null引數,或者任何其它從 exceptionally() 方法中返回的值。
responseFuture .exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null; }) .thenAccept(this::send); //probably not what you think
丟失一秒超時的問題非常巧妙。我們原始的程式碼在Future完成之前最多等待(阻塞)1秒,否則就會丟擲 TimeoutException。我們丟失了這個功能,更糟糕的是,單元測試超時的不是很方便,經常會跳過這個環節。為了維持超時機制,而又不破壞事件驅動的原則,我們需要建立一個額外的模組:一個在給定時間後必定會失敗的 Future。
public static <T> CompletableFuture<T> failAfter(Duration duration) { final CompletableFuture<T> promise = new CompletableFuture<>(); scheduler.schedule(() -> { final TimeoutException ex = new TimeoutException("Timeout after " + duration); return promise.completeExceptionally(ex); }, duration.toMillis(), MILLISECONDS); return promise; } private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( 1, new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("failAfter-%d") .build());
這個很簡單:我們建立一個promise(沒有後臺任務或執行緒池的 Future),然後在給定的 java.time.Duration 之後會丟擲 TimeoutException 異常。如果在某個地方呼叫 get() 獲取這個 Future,阻塞的時間到達這個指定的時間後會丟擲 TimeoutException。
實際上,它是一個包裝了 TimeoutException 的 ExecutionException,這個無需多說。注意,我使用了固定一個執行緒的執行緒池。這不僅僅是為了教學的目的:這是“1個執行緒應當能滿足任何人的需求”的場景。failAfter() 本身沒多大的用處,但是如果和 ourresponseFuture 一起使用,我們就能解決這個問題了。
final CompletableFuture<Response> responseFuture = asyncCode(); final CompletableFuture<Response> oneSecondTimeout = failAfter(Duration.ofSeconds(1)); responseFuture .acceptEither(oneSecondTimeout, this::send) .exceptionally(throwable -> { log.error("Problem", throwable); return null; });
這裡還做了很多其他事情。在後臺的任務接收 responseFuture 時,我們也建立了一個“合成”的 oneSecondTimeout future,這在成功的時候永遠不會執行,但是在1秒後就會導致任務失敗。現在我們聯合這兩個叫做 acceptEither,這個操作將執行先完成 Future 的程式碼塊,而簡單的忽略 responseFuture 或 oneSecondTimeout 中執行比較慢的那個。如果 asyncCode() 程式碼在1秒內執行完成,this::send 就會被呼叫,而 oneSecondTimeout 異常就不會丟擲。但是,如果 asyncCode() 執行真的很慢,oneSecondTimeout 異常就先丟擲。由於一個異常導致任務失敗,exceptionallyerror 處理器就會被呼叫,而不是 this::send 方法。你可以選擇執行 send() 或者 exceptionally,但是不能兩個都執行。當如,如果我們有兩個“普通”的 Future 正常執行完成了,則最先響應的那個將呼叫 send() 方法,後面的就會被丟棄。
這個不是最清晰的解決方案。更清晰的方案是包裝原始的 Future,然後保證它能在給定的時間內執行。這種操作對 com.twitter.util.Future 是可行的(Scala叫做 within()),但是 scala.concurrent.Future 中沒有這個功能(據推測是為了鼓勵使用前面的方式)。我們暫時不討論Scala背後如何執行的,先實現類似 CompletableFuture 的操作。它接受一個 Future 作為輸入,然後返回一個 Future,這個 Future 在後臺任務完成時候執行完成。但是,如果底層的 Future 執行的時間太長,就或丟擲異常:
public static <T> CompletableFuture<T> within(CompletableFuture<T> future, Duration duration) { final CompletableFuture<T> timeout = failAfter(duration); return future.applyToEither(timeout, Function.identity()); }
這引導我們實現最終的、清晰的、靈活的方法:
final CompletableFuture<Response> responseFuture = within( asyncCode(), Duration.ofSeconds(1)); responseFuture .thenAccept(this::send) .exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null; });
希望你喜歡這篇文章,因為你已經知道在Java裡,實現響應式程式設計不再是什麼問題。
相關文章
- Java中的CompletableFuture超時使用Java
- Java CompletableFuture 非同步超時實現探索Java非同步
- Java中的任務超時處理Java
- Java 專案中使用 Resilience4j 框架實現非同步超時處理Java框架非同步
- 處理可能超時的非同步操作非同步
- 【轉】JAVA處理執行緒超時Java執行緒
- Java中對時間的處理Java
- Java中的非同步程式設計與CompletableFuture應用Java非同步程式設計
- Java8中的時間處理Java
- JavaScript 處理WebSocket的超時JavaScriptWeb
- java時間處理Java
- Java如何使用實時流式計算處理?Java
- 【Java分享客棧】一文搞定CompletableFuture並行處理,成倍縮短查詢時間。Java並行
- PHP超時處理全面總結PHP
- Java中ExecutorService與CompletableFuture指南Java
- java同步非阻塞IOJava
- Go併發呼叫的超時處理Go
- Nginx 超時事件的處理機制Nginx事件
- iOS資料請求超時處理iOS
- RSA 非對稱加密&解密,超長字串分塊處理加密解密字串
- 從同步原語看非阻塞同步以及Java中的應用Java
- 如何解讀 Java IO、NIO 中的同步阻塞與同步非阻塞?Java
- 非同步技巧之CompletableFuture非同步
- [轉] Scala 中的非同步事件處理非同步事件
- java中的事件處理Java事件
- java之使用CompletableFuture入門2Java
- 使用Jackson在Java中處理JSONJavaJSON
- JAVA不使用執行緒池來處理的非同步的方法Java執行緒非同步
- java 非同步查詢轉同步多種實現方式:迴圈等待,CountDownLatch,Spring EventListener,超時處理和空迴圈效能優化Java非同步CountDownLatchSpring優化
- pip安裝模組超時怎麼處理
- PHP中如何處理時區PHP
- java8 之CompletableFuture -- 如何構建非同步應用Java非同步
- Java非同步程式設計:CompletableFuture與Future的對比Java非同步程式設計
- 非同步程式設計 CompletableFuture非同步程式設計
- 非同步神器:CompletableFuture實現原理和使用場景非同步
- java當中的批處理Java
- Java 中的並行處理Java並行
- 非同步處理方法非同步