文件

§處理非同步結果

§讓控制器變成非同步

在內部,Play Framework 從底層到頂層都是非同步的。Play 以非同步、非封鎖的方式處理每個要求。

預設組態是針對非同步控制器調整的。換句話說,應用程式程式碼應該避免在控制器中封鎖,也就是說,讓控制器程式碼等待一個操作。此類封鎖操作的常見範例包括 JDBC 呼叫、串流 API、HTTP 要求和長時間運算。

儘管可以增加預設執行緒的數量,以允許更多同時要求由封鎖控制器處理,但遵循建議的非同步控制器方法,可以更輕鬆地擴充系統,並在負載下保持系統回應。

§建立非封鎖動作

由於 Play 的運作方式,動作程式碼必須盡可能快,也就是說,非封鎖。那麼,如果我們還無法計算結果,我們應該從動作中傳回什麼?我們應該傳回結果的「承諾」!

Java 8 和更新版本提供一個稱為 CompletionStage 的通用承諾 API。CompletionStage<Result> 最終將以類型為 Result 的值兌現。透過使用 CompletionStage<Result> 而不是一般的 Result,我們能夠快速從動作中傳回,而不會封鎖任何內容。Play 將在承諾兌現後立即提供結果。

§如何建立 CompletionStage<Result>

要建立 CompletionStage<Result>,我們首先需要另一個承諾:將提供我們計算結果所需實際值的承諾

CompletionStage<Double> promiseOfPIValue = computePIAsynchronously();
// Runs in same thread
CompletionStage<Result> promiseOfResult =
    promiseOfPIValue.thenApply(pi -> ok("PI value computed: " + pi));

Play 非同步 API 方法會提供 CompletionStage。這是您使用 play.libs.WS API 呼叫外部網路服務,或使用 Pekko 排程非同步任務或使用 play.libs.Pekko 與 Actor 溝通時的情況。

在此情況下,使用 CompletionStage.thenApply 會在與前一個任務相同的呼叫執行緒中執行完成階段。當您有少量未封鎖的 CPU 繫結邏輯時,這很不錯。

非同步執行程式碼區塊並取得 CompletionStage 的簡單方法是使用 CompletableFuture.supplyAsync() 方法

// creates new task
CompletionStage<Integer> promiseOfInt =
    CompletableFuture.supplyAsync(() -> intensiveComputation());

使用 supplyAsync 會建立一個新的任務,該任務將放置在 fork join 池中,並且可以從不同的執行緒呼叫 - 儘管在此處它使用預設執行器,而且在實務上您將明確指定執行器。

只有 CompletionStage 中的“*Async”方法提供非同步執行。

§使用 ClassLoaderExecutionContext

Action 內部使用 Java CompletionStage 時,您必須明確提供類別載入器執行緒環境作為執行器,以確保類別載入器保持在範圍內。

您可以透過依賴注入提供 play.libs.concurrent.ClassLoaderExecutionContext 執行個體

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.ClassLoaderExecutionContext;
import play.mvc.*;

public class MyController extends Controller {

  private ClassLoaderExecutionContext clExecutionContext;

  @Inject
  public MyController(ClassLoaderExecutionContext ec) {
    this.clExecutionContext = ec;
  }

  public CompletionStage<Result> index() {
    // Use a different task with explicit EC
    return calculateResponse()
        .thenApplyAsync(
            answer -> {
              return ok("answer was " + answer).flashing("info", "Response updated!");
            },
            clExecutionContext.current());
  }

  private static CompletionStage<String> calculateResponse() {
    return CompletableFuture.completedFuture("42");
  }
}

請參閱 類別載入器 以取得有關使用 ClassLoaderExecutionContext 的更多資訊。

§使用 CustomExecutionContext 和 ClassLoaderExecution

使用 CompletionStageClassLoaderExecutionContext 只是其中一部分!在這個時候,您仍然在 Play 的預設 ExecutionContext 中。如果您呼叫 JDBC 等封鎖 API,那麼您仍然需要讓您的 ExecutionStage 使用不同的執行器執行,以將其移出 Play 的呈現執行緒池。您可以透過建立 play.libs.concurrent.CustomExecutionContext 的子類別,並參考 自訂調度器 來執行此操作。

新增下列匯入

import play.libs.concurrent.ClassLoaderExecution;

import javax.inject.Inject;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletionStage;
import static java.util.concurrent.CompletableFuture.supplyAsync;

定義自訂執行緒環境

public class MyExecutionContext extends CustomExecutionContext {

  @Inject
  public MyExecutionContext(ActorSystem actorSystem) {
    // uses a custom thread pool defined in application.conf
    super(actorSystem, "my.dispatcher");
  }
}

您需要在 application.conf 中定義自訂調度器,這可透過 Pekko 調度器設定 來完成。

在您擁有自訂調度器後,新增明確執行器並使用 ClassLoaderExecution.fromThread 包裝它

public class Application extends Controller {

  private MyExecutionContext myExecutionContext;

  @Inject
  public Application(MyExecutionContext myExecutionContext) {
    this.myExecutionContext = myExecutionContext;
  }

  public CompletionStage<Result> index() {
    // Wrap an existing thread pool, using the context from the current thread
    Executor myEc = ClassLoaderExecution.fromThread(myExecutionContext);
    return supplyAsync(() -> intensiveComputation(), myEc)
        .thenApplyAsync(i -> ok("Got result: " + i), myEc);
  }

  public int intensiveComputation() {
    return 2;
  }
}

您無法透過將同步 IO 包裝在 CompletionStage 中,神奇地將其轉換為非同步。如果您無法變更應用程式的架構以避免封鎖操作,在某個時間點上該操作必須執行,而該執行緒將會封鎖。因此,除了在 CompletionStage 中封裝操作外,還需要將其設定為在已設定為擁有足夠執行緒以處理預期並行性的獨立執行緒環境中執行。請參閱 了解 Play 執行緒池 以取得更多資訊,並下載顯示資料庫整合的 play 範例範本

§預設情況下動作為非同步

Play 動作 預設為非同步。例如,在下列控制器程式碼中,傳回的 Result 內部封裝在承諾中

public Result index(Http.Request request) {
  return ok("Got request " + request + "!");
}

注意:無論動作程式碼傳回 ResultCompletionStage<Result>,這兩種傳回物件在內部都會以相同方式處理。只有一種 Action,它是非同步的,而不是兩種(一種同步,一種非同步)。傳回 CompletionStage 是一種撰寫非封鎖程式碼的技巧。

§處理逾時

妥善處理逾時情況通常很有用,這樣可以避免在發生問題時網頁瀏覽器會遭到封鎖並等待。你可以使用 play.libs.concurrent.Futures.timeout 方法來封裝一個非封鎖逾時中的 CompletionStage

class MyClass {

  private final Futures futures;
  private final Executor customExecutor = ForkJoinPool.commonPool();

  @Inject
  public MyClass(Futures futures) {
    this.futures = futures;
  }

  CompletionStage<Double> callWithOneSecondTimeout() {
    return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
  }

  public CompletionStage<String> delayedResult() {
    long start = System.currentTimeMillis();
    return futures.delayed(
        () ->
            CompletableFuture.supplyAsync(
                () -> {
                  long end = System.currentTimeMillis();
                  long seconds = end - start;
                  return "rendered after " + seconds + " seconds";
                },
                customExecutor),
        Duration.of(3, SECONDS));
  }
}

注意:逾時與取消不同,即使在逾時的情況下,指定的未來仍會完成,即使未傳回已完成的值。

下一頁:串流 HTTP 回應


在此文件發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,請隨時貢獻一個 pull request。有問題或建議要分享?請前往 我們的社群論壇 與社群展開對話。