一句话总结
- 阻塞:线程被卡住,彻底躺平,什么事都干不了(像堵车时的司机)。
- 挂起:协程暂停,但线程立马去干别的活(像司机绕路走,不堵在原地)。
一、阻塞:资源浪费与性能杀手
阻塞(Blocking) 是传统同步编程中,线程等待某个操作完成的常见状态。当线程被阻塞时,它会一直占用 CPU 资源,直到操作完成。
-
本质:线程在等待时,其状态从“运行”变为“等待”。CPU 调度器无法将该线程分配给其他任务,导致资源浪费。
-
主要危害:
- UI 线程卡死:在 Android 中,如果 UI 线程被阻塞,它将无法响应用户输入和刷新界面,导致应用无响应(ANR)。
- 线程池耗尽:在高并发场景下,如果大量请求都阻塞在 I/O 操作上,会迅速耗尽线程池,导致后续请求无法被处理。
-
常见阻塞操作:
Thread.sleep()、同步的网络请求、文件 I/O、数据库查询等。
二、挂起:非阻塞式的高效并发
挂起(Suspending) 是 Kotlin 协程中的核心概念。当一个协程被“挂起”时,它会暂停自身的执行,但并不会阻塞底层线程。
- 本质:当协程遇到一个挂起点(如
delay()),它会立即释放所占用的线程,让该线程可以去执行其他任务。当挂起条件满足后,协程会从挂起点恢复,继续执行后续代码。这个过程由协程框架和编译器在后台悄无声息地完成。 - 核心魔法:编译器状态机:
suspend关键字告诉编译器,这个函数可能会在执行过程中暂停。编译器会自动将挂起函数转换为一个状态机。它会保存协程在挂起时的所有上下文信息,并在恢复时精确地回到原来的位置。这使得我们可以用同步的、顺序的思维来编写异步代码。
三、实践中的取舍:何时阻塞,何时挂起?
| 挂起 | 阻塞 | |
|---|---|---|
| 线程状态 | 线程被释放,可用于其他任务 | 线程被占用,无法做任何事 |
| 资源消耗 | 轻量,一个线程可处理数千协程 | 重量,一个线程只能处理一个任务 |
| 适用场景 | 适用于非阻塞式的异步操作,如网络请求、数据库访问。 | 仅在需要同步等待的特定场景下使用,且必须在后台线程进行,如传统同步 I/O。 |
| 代码范例 | suspend fun fetchData() { ... } | Thread.sleep(1000) |
四、强制线程切换:挂起与阻塞的桥梁
尽管挂起是非阻塞的,但在协程中调用传统的阻塞函数(如文件读写)仍然会阻塞协程所在的线程。为了解决这个问题,我们需要借助协程调度器(Dispatcher) 来进行线程切换。
Dispatchers.IO:这是一个专为阻塞式 I/O 操作设计的线程池。withContext():这是一个挂起函数,它可以切换协程的上下文。
通过使用 withContext(Dispatchers.IO),我们可以将一个阻塞操作安全地转移到一个专用的 I/O 线程池中执行,从而避免阻塞主线程或默认线程池。
// 在协程中安全地执行阻塞I/O操作
suspend fun saveFile(data: String) {
withContext(Dispatchers.IO) { // 切换到I/O线程
// 这里的代码是阻塞的,但它在独立的线程池中运行
File("file.txt").writeText(data)
}
}
五、总结
阻塞是同步编程的资源杀手,它让线程白白等待。而挂起则是 Kotlin 协程的核心魔法,它使得我们可以用优雅的同步代码,实现高效的异步并发。理解并正确运用这两个概念,是编写健壮、高性能应用的基石。