1. 概述

在现代编程中,应用程序往往需要支持并发与并行处理。本文将探讨 Scala 中的异步编程方式,并介绍如何以同步的方式处理异步结果。

2. Future 简介

直接操作线程在大型项目中会变得非常麻烦。为了简化异步代码的编写,Scala 提供了 Future API,它为异步编程提供了函数式抽象。

一个 Future 是一个占位符对象,代表一个可能尚未完成的计算结果。它允许我们以简单的方式并发执行代码。一旦创建,Future 就会立即在后台运行,并在未来的某个时间点返回结果

我们可以通过调用 Future 对象的 apply 方法来创建一个 Future。例如,我们想模拟从 URL 获取数据的过程(为了测试,这里不真正调用网络):

def fetchDataFrom(url : String, waitTime : Long = 0L) : Future[String] = Future {
  Thread.sleep(waitTime)
  s"Mock response from $url"
}
fetchDataFrom("https://www.baeldung.com")

不过,这段代码会报错:

Cannot find an implicit ExecutionContext. You might pass an (implicit ec: ExecutionContext) parameter to your method
or import scala.concurrent.ExecutionContext.Implicits.global.
  Future {

编译器提示我们缺少 ExecutionContext。因为 Future 是在另一个线程中执行的,而 ExecutionContext 正是提供线程调度能力的抽象。我们需要显式提供或导入默认的全局上下文:

import scala.concurrent.ExecutionContext.Implicits.global

3. Future 的完成处理

由于 Future 是在后台线程执行的,主线程可以继续执行其他任务。在前面的例子中,如果主线程没有其他工作,程序可能会提前结束,而 Future 还未完成。

✅ **一个 Future 的结果是一个 *Try*,表示计算成功或失败的状态**。

那我们怎么拿到 Future 的结果呢?一种“土办法”是使用 Thread.sleep 来强制主线程等待:

fetchDataFrom("https://www.baeldung.com").onSuccess(result => println(result))

Thread.sleep(1000)

⚠️ 但这种方式很不优雅,因为我们无法预知异步任务到底要执行多久。

更合理的方式是让主线程“等待” Future 完成后再继续执行,这时候就可以用到 Await API。

3.1. Await.ready

Await.ready 方法会阻塞当前线程,直到 Future 完成或超时。

来看一个例子:

val fut = fetchDataFrom("https://www.baeldung.com")
fut.isCompleted shouldBe false

val completedFuture: Future[String] = Await.ready(fut, 2.seconds)

fut shouldBe completedFuture
completedFuture.isCompleted shouldBe true
completedFuture.isInstanceOf[Future[String]] shouldBe true
val assertion = completedFuture.value match {
 case Some(result) => result.isSuccess
 case _ => false
}
assertion shouldBe true

Await.ready 接收两个参数:

  • 第一个是待等待的 Future
  • 第二个是最大等待时间

⚠️ 注意,这不是简单的 Thread.sleep。如果 Future 在指定时间内未完成,会抛出 TimeoutException

✅ **Await.ready 返回的是原始的已完成的 Future,其结果是 SuccessFailure,调用方需要自行处理结果**。

3.2. Await.result

Await.resultAwait.ready 类似,也是阻塞当前线程直到 Future 完成或超时。但它会尝试直接提取 Future 中的值。

val fut = fetchDataFrom("https://www.baeldung.com")
fut.isCompleted shouldBe false
val completedFutureResult: String = Await.result(fut, 2.seconds)
completedFutureResult.isInstanceOf[String] shouldBe true

⚠️ 注意,返回值是 String,而不是 Future[String]

✅ 你也可以用 Await.ready 来模拟 Await.result

val result: String = Await.ready(fut, 5.seconds).value.get.get

但直接用 Await.result 有个坑:如果 Future 以异常结束(即 Failure),程序会直接抛出该异常并崩溃。

❌ 所以,除非你非常确定 Future 不会失败,否则不推荐直接使用 Await.result

4. Await.result vs. Await.ready

特性 Await.ready Await.result
阻塞行为 等待 Future 完成或超时 同上
返回值 返回原始 Future 提取 Future 中的值
异常处理 需手动判断 Success / Failure 异常直接抛出,可能 crash 程序
使用建议 更安全,适合不确定结果的场景 只在明确成功时使用

来看个例子:

def futureWithoutException(): Future[String] = Future {
  "Hello"
}
def futureWithException(): Future[String] = Future {
  throw new NullPointerException()
}

使用 Await.readyAwait.result 的区别如下:

val f1 = Await.ready(futureWithoutException, 2.seconds)
assert(f1.isInstanceOf[Future[String]] && f1.value.get.get.contains("Hello"))

val f2 = Await.ready(futureWithException, 2.seconds)
assert(f2.isInstanceOf[Future[String]] && f2.value.get.failed.get.isInstanceOf[NullPointerException])

val f3 = Await.result(futureWithoutException, 2.seconds)
assert(f3.isInstanceOf[String] && f3.contains("Hello"))

assert (intercept[Exception] { Await.result(futureWithException, 2.seconds)}.isInstanceOf[NullPointerException]) 

✅ 从上面的例子可以看出,使用 Await.result 时,如果 Future 失败,程序会直接抛出异常。

⚠️ 此外,使用 Await.result 时,很难区分是计算失败了,还是超时了。

📌 **建议:除非万不得已,否则尽量避免使用 Await**。异步编程的核心是避免阻塞,使用 Await 会破坏这一原则。

5. 总结

本文介绍了 Scala 中异步编程的基础,以及如何通过 Await.readyAwait.result 同步处理 Future 的结果。我们还比较了这两个方法的差异,强调了在使用 Await.result 时需要注意异常处理的问题。

📌 要点回顾:

  • Future 是异步计算的占位符
  • ✅ 使用 Await.ready 更安全,返回原始 Future
  • ❌ 使用 Await.result 要小心异常,可能 crash 程序
  • ⚠️ Await 会阻塞线程,违背异步设计初衷,慎用

源码可在 GitHub 获取。


原始标题:Creational Design Patterns in Kotlin: Builder