阻塞 vs. 挂起:Kotlin协程的并发核心

581 阅读3分钟

一句话总结

  • 阻塞:线程被卡住,彻底躺平,什么事都干不了(像堵车时的司机)。
  • 挂起:协程暂停,但线程立马去干别的活(像司机绕路走,不堵在原地)。

一、阻塞:资源浪费与性能杀手

阻塞(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 协程的核心魔法,它使得我们可以用优雅的同步代码,实现高效的异步并发。理解并正确运用这两个概念,是编写健壮、高性能应用的基石。