文件

§處理非同步結果

§讓控制器變成非同步

Play Framework 從底層到上層都是非同步的。Play 以非同步、非阻擋的方式處理每個要求。

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

雖然可以增加預設執行內容中的執行緒數目,以允許封鎖控制器處理更多並行要求,但遵循建議的方式讓控制器保持非同步,可以更輕鬆地擴充系統,並在負載下保持系統回應。

§建立非封鎖動作

由於 Play 的運作方式,動作程式碼必須盡可能快速,也就是非封鎖。因此,如果我們還無法產生結果,應該回傳什麼結果?答案是未來結果!

Future[Result] 最終會以 Result 類型的值兌現。透過提供 Future[Result] 而不是一般的 Result,我們可以快速產生結果,而不封鎖。Play 會在承諾兌現後提供結果。

網頁用戶端會在等待回應時封鎖,但伺服器上不會封鎖任何項目,而且伺服器資源可以用來提供服務給其他用戶端。

使用 Future 只是其中一部分!如果您呼叫封鎖 API(例如 JDBC),則仍然需要讓您的 ExecutionStage 使用不同的執行器執行,才能將它移出 Play 的呈現執行緒池。您可以透過建立 play.api.libs.concurrent.CustomExecutionContext 的子類別,並參考自訂調度器來執行此動作。

import play.api.libs.concurrent.CustomExecutionContext

// Make sure to bind the new context class to this trait using one of the custom
// binding techniques listed on the "Scala Dependency Injection" documentation page
trait MyExecutionContext extends ExecutionContext

class MyExecutionContextImpl @Inject() (system: ActorSystem)
    extends CustomExecutionContext(system, "my.executor")
    with MyExecutionContext

class HomeController @Inject() (myExecutionContext: MyExecutionContext, val controllerComponents: ControllerComponents)
    extends BaseController {
  def index = Action.async {
    Future {
      // Call some blocking API
      Ok("result of blocking call")
    }(myExecutionContext)
  }
}

請參閱 ThreadPools 以取得有關有效使用自訂執行緒環境的更多資訊。

§如何建立一個 Future[Result]

要建立一個 Future[Result],我們首先需要另一個 future:這個 future 會提供我們計算結果所需的實際值


val futurePIValue: Future[Double] = computePIAsynchronously() val futureResult: Future[Result] = futurePIValue.map { pi => Ok("PI value computed: " + pi) }

Play 的所有非同步 API 呼叫都會提供一個 Future。無論您是使用 play.api.libs.WS API 呼叫外部網路服務,或是使用 Pekko 排程非同步工作或使用 play.api.libs.Pekko 與 actor 進行通訊,情況皆是如此。

以下是執行程式區塊的非同步方式,以及取得 Future 的簡單方法

val futureInt: Future[Int] = scala.concurrent.Future {
  intensiveComputation()
}

注意:了解 future 在哪個執行緒上執行程式碼非常重要。在上述兩個程式區塊中,會匯入 Play 預設的執行緒環境。這是一個隱含參數,會傳遞給 future API 中所有接受回呼的函式。執行緒環境通常等於執行緒池,但並非一定如此。

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

使用 Actor 處理封鎖作業也可能有所幫助。Actor 提供一個乾淨的模型,用於處理逾時和失敗、設定封鎖執行緒環境,以及管理可能與服務相關聯的任何狀態。此外,Actor 還提供像 ScatterGatherFirstCompletedRouter 這樣的模式,用於處理同時快取和資料庫要求,並允許在後端伺服器叢集上執行遠端執行。但根據您的需求,Actor 可能會過於複雜。

§傳回未來

雖然我們到目前為止都使用 Action.apply 建構函式方法來建構動作,但要傳送非同步結果,我們需要使用 Action.async 建構函式方法

def index = Action.async {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  futureInt.map(i => Ok("Got result: " + i))
}

§動作預設為非同步

Play 動作預設為非同步。例如,在以下控制器程式碼中,程式碼的 { Ok(...) } 部分並非控制器的函式主體。它是一個匿名函式,傳遞給 Action 物件的 apply 方法,它會建立一個 Action 類型的物件。在內部,您編寫的匿名函式將會被呼叫,其結果將會封裝在 Future 中。

def echo: Action[AnyContent] = Action { request => Ok("Got request [" + request + "]") }

注意: Action.applyAction.async 都會建立 Action 物件,其內部處理方式相同。只有一種 Action,它是非同步的,而不是兩種(同步和非同步)。.async 建構函式只是一個簡化建立基於傳回 Future 的 API 的動作的工具,這使得撰寫非封鎖程式碼變得更容易。

§處理逾時

妥善處理逾時通常很有用,以避免在發生問題時讓網路瀏覽器封鎖並等待。您可以使用 play.api.libs.concurrent.FuturesFuture 封裝在非封鎖逾時中。

import scala.concurrent.duration._
import play.api.libs.concurrent.Futures._

def index = Action.async {
  // You will need an implicit Futures for withTimeout() -- you usually get
  // that by injecting it into your controller's constructor
  intensiveComputation()
    .withTimeout(1.seconds)
    .map { i => Ok("Got result: " + i) }
    .recover {
      case e: scala.concurrent.TimeoutException =>
        InternalServerError("timeout")
    }
}

注意: 逾時與取消不同 - 即使逾時,給定的未來仍會完成,即使未傳回已完成的值。

下一步:串流 HTTP 回應


在此文件發現錯誤?此頁面的原始程式碼可在此處找到 按此。在閱讀 文件指南 後,請隨時提出協力請求。有問題或建議想分享?請前往 我們的社群論壇 與社群展開對話。