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,其结果是 Success
或 Failure
,调用方需要自行处理结果**。
3.2. Await.result
Await.result
与 Await.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.ready
和 Await.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.ready
和 Await.result
同步处理 Future 的结果。我们还比较了这两个方法的差异,强调了在使用 Await.result
时需要注意异常处理的问题。
📌 要点回顾:
- ✅ Future 是异步计算的占位符
- ✅ 使用
Await.ready
更安全,返回原始 Future - ❌ 使用
Await.result
要小心异常,可能 crash 程序 - ⚠️
Await
会阻塞线程,违背异步设计初衷,慎用
源码可在 GitHub 获取。