协程的基本概念及用法


协程的基本概念及用法

  • 协程的基本概念及用法
  • 1. 阅读前问题卡:Kotlin 协程
    • 1.1 阅读前先看这几个问题
    • 1.2 读完后完成这 3 个任务
      • 1.2.1 任务 1:说明协程与线程的关系
      • 1.2.2 任务 2:区分协程创建、切换与挂起
      • 1.2.3 任务 3:走读请求示例的状态变化
    • 1.3 自检清单
  • 2. 协程是什么?
  • 3. 协程与线程的关系
  • 4. 协程的基本使用
    • 4.1 使用 launch 方法
    • 4.2 使用 withContext 方法
    • 4.3 使用 suspend 关键字
  • 5. 获取协程的返回值
    • 5.1 async / await
    • 5.2 suspendCoroutine
  • 6. 协程的非阻塞式挂起
  • 7. 代码示例


1. 阅读前问题卡:Kotlin 协程

建议先读:先读“协程是什么?”和“协程与线程的关系”,再依次阅读launchwithContextsuspend三个小节。


1.1 阅读前先看这几个问题

  1. 文档如何界定 Kotlin 协程,它与线程和进程分别是什么关系?
  2. 为什么单线程中使用协程不会缩短总执行时间,Android 主线程上的耗时操作又为什么仍需切换线程?
  3. runBlockingGlobalScope.launch和自行创建CoroutineScope这 3 种方式各有什么适用边界?
  4. launchwithContextsuspend分别负责创建协程、切换线程和限制调用位置中的哪一部分?
  5. 协程执行到suspend方法时,线程和协程各自如何继续执行,完成后又如何resume
  6. async / awaitsuspendCoroutine在是否创建协程、返回结果和提交异常方面有什么区别?
  7. 代码示例中的请求状态如何从IDLE变为LOADING,再进入SUCCESSFAIL,期间在哪些线程执行?


1.2 读完后完成这 3 个任务


1.2.1 任务 1:说明协程与线程的关系
  • 做什么:用自己的话复述协程、线程和进程的关系,并解释文档所说的“协作式多任务”。
  • 验证标准:能用不超过 4 句话说明代码运行层级、协程依赖线程、单线程总耗时和 Android 主线程切换要求。


1.2.2 任务 2:区分协程创建、切换与挂起
  • 做什么:整理一张小表,对比runBlockingGlobalScope.launchCoroutineScope.launchwithContextsuspend的作用。
  • 验证标准:每项都能写出“是否创建协程、是否切换线程、是否阻塞或限制调用位置”中的适用结论,并能对应到正文依据。


1.2.3 任务 3:走读请求示例的状态变化
  • 做什么:从simulateNetworkRequest()开始,按执行顺序标出状态更新、协程启动、IO 线程切换、结果返回和 Compose 界面读取状态的位置。
  • 验证标准:能画出IDLE → LOADING → SUCCESS / FAIL的状态路径,并指出viewModelScope.launchasync / awaitwithContext(Dispatchers.IO)collectAsState()各自所在步骤。


1.3 自检清单

  • 我能用一句话说明本文语境下的 Kotlin 协程是什么。
  • 我能说明协程、线程和进程之间的关系。
  • 我能区分 3 种协程创建方式的特点和使用边界。
  • 我能解释launchwithContextsuspend、挂起和恢复之间的关系。
  • 我能比较async / awaitsuspendCoroutine的用途。
  • 我能沿着示例代码复述请求状态、线程切换和界面更新流程。


2. 协程是什么?

协程是一种编程思想,并不局限于特定的语言。除 Kotlin 以外,其他的一些语言,如 Go、Python 等都可以在语言层面上实现协程。

Kotlin Coroutine 本质上是 Kotlin 官方提供的一套线程封装 API,其设计初衷是为了解决并发问题,让“协作式多任务”实现起来更方便。


3. 协程与线程的关系

从 Android 开发者的角度去理解它们之间的关系:

  • 我们所有的代码跑在线程中,而线程跑在进程中。
  • 协程也是跑在线程中的,可以是单线程,也可以是多线程。
  • 单线程中,协程的总执行时间并不会比不用协程少。
  • Android 系统上,在主线程上进行耗时操作(如网络请求),即使用了协程,也需要切换线程。


4. 协程的基本使用


4.1 使用 launch 方法

协程在写法上和普通的顺序代码类似,可以让开发者用同步的方式写出异步的代码。创建协程可以使用以下 3 种方式:

runBlocking{// 方法1:使用 runBlocking 顶层函数}GlobalScope.launch{// 方法2:使用 GlobalScope 单例对象,调用 launch 开启协程}valcoroutineScope=CoroutineScope(context)coroutineScope.launch{// 方法3:自行通过 CoroutineContext 创建一个 CoroutineScope 对象}
  • 方法 1 适用于单元测试场景,实际开发中不使用,因为它是线程阻塞的。
  • 方法 2 与runBlocking相比不会阻塞线程,但它的生命周期会和 APP 一致,且无法取消。
  • 方法 3 比较推荐使用,可以通过context参数去管理和控制协程的生命周期。

此处的launch方法含义是:创建一个新的协程,并在指定的线程上运行它。传给launch方法的连续代码段就被叫做一个协程,传给launch方法的方法参数可以用于指定执行这段代码的线程。

coroutineScope.launch(Dispatchers.IO){// 可以通过 Dispatchers.IO 参数把任务切到 IO 线程执行}coroutineScope.launch(Dispatchers.Main){// 也可以通过 Dispatchers.Main 参数切换到主线程}


4.2 使用 withContext 方法

这个方法可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切换回去继续执行,如下所示:

coroutineScope.launch(Dispatchers.Main){// 在 UI 线程开始valimage=withContext(Dispatchers.IO){// 切换到 IO 线程getImage(imageId)// 在 IO 线程执行}imageView.setImageBitmap(image)// 回到 UI 线程更新 UI}

该方法支持自动切回原来的线程,能够消除并发代码在协作时产生的嵌套。如果需要频繁地进行线程切换,这种写法将有很大的优势,这就是“使用同步的方式写异步代码”。


4.3 使用 suspend 关键字

我们可以把withContext单独放进一个方法里面,此方法需要使用suspend关键字标记才能编译通过:

suspendfungetImage(imageId:Int)=withContext(Dispatchers.IO){}

使用launchasync等方法创建的协程,在执行到某个suspend方法时会从正在执行它的线程上脱离。互相脱离后的线程和协程将会分别执行不同的任务:

  • 线程:线程执行到了suspend方法,就暂时不再执行剩余协程代码,跳出协程的代码块。如果它是一个后台线程,它会被系统回收或者再利用(继续执行别的后台任务),与 Java 线程池中的线程等同;如果它是 Android 主线程,它会继续执行界面刷新任务。
  • 协程:协程会从上面被挂起的suspend方法开始,在该方法的参数指定的线程(如Dispatchers.IO所指定的 IO 线程)中继续往下执行。suspend方法执行完成之后,会重新切换回它原先的线程。这个“切回来”的动作,在 Kotlin 中叫做resume

suspend关键字只是一个提醒,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的suspend方法。该关键字本身只有一个效果:限制这个方法只能在协程里或者另一个suspend方法中被调用,否则就会编译不通过。


5. 获取协程的返回值

协程是一种异步的概念,需要一些特殊操作才能获取返回值。获取协程的返回值可以使用以下方式:


5.1 async / await

主要流程是使用async开启协程,然后调用async返回的Deferred对象的await方法获取协程运算的结果:

coroutineScope.launch(Dispatchers.IO){valjob=async{delay(1000)return@async"return value"}println("async result=${job.await()}")}


5.2 suspendCoroutine

async不同,suspendCoroutine只是一个挂起方法,无法开启协程,需要在其他协程作用域中使用。协程运行结束后,使用resume提交返回值或使用resumeWithException抛出异常。

coroutineScope.launch(Dispatchers.IO){try{valresult=suspendCoroutine<String>{delay(1000)valrandom=Random().nextBoolean()if(random){it.resume("return value")}else{it.resumeWithException(Exception("Coroutine Failure"))}}println("suspendCoroutine success result:$result")}catch(e:java.lang.Exception){println("suspendCoroutine failure exception:$e")}}


6. 协程的非阻塞式挂起

“非阻塞式挂起”指的就是协程在挂起的同时切线程这件事情。使用了协程的代码看似阻塞,但由于协程内部做了很多工作(包括自动切换线程),它实际上是非阻塞的。在代码执行的过程中,线程虽然会切换,但写法上类似普通的单线程代码。

在 Kotlin 中,协程就是基于线程来实现的一种更上层的工具 API,类似于 Android 自带的 Handler 系列 API。在设计思想上,协程是一个基于线程的上层框架。Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,只是简化了多线程的开发。


7. 代码示例

使用协程模拟实现一个网络请求,等待时显示 Loading,请求成功或者出错让 Loading 消失,并将状态反馈给用户。

ViewModel中编写如下业务逻辑代码:

@HiltViewModelclassMainViewModel@Injectconstructor():ViewModel(){enumclassRequestStatus{IDLE,LOADING,SUCCESS,FAIL}valrequestStatus=MutableStateFlow(RequestStatus.IDLE)/** * 模拟网络请求,并将状态设置给 requestStatus 变量 */funsimulateNetworkRequest(){requestStatus.value=RequestStatus.LOADING viewModelScope.launch{valrequestResult=async{performSimulatedRequest()}.await()requestStatus.value=if(requestResult)RequestStatus.SUCCESSelseRequestStatus.FAIL}}/** * 使用 delay 方法模拟耗时操作,用随机数模拟请求成功或失败 */privatesuspendfunperformSimulatedRequest()=withContext(Dispatchers.IO){delay(500)valrandom=Random()return@withContextrandom.nextBoolean()}}

MainActivity中使用 Jetpack Compose,将请求状态实时显示在界面上:

@AndroidEntryPointclassMainActivity:ComponentActivity(){privatevalmainViewModel:MainViewModelbyviewModels()overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{ComposeTheme{Surface(modifier=Modifier.fillMaxSize(),color=MaterialTheme.colorScheme.background){valrequestStatusState=mainViewModel.requestStatus.collectAsState()valrequestStatusbyrememberSaveable{requestStatusState}Text(text=requestStatus.name,color=Color.Red)}}}mainViewModel.simulateNetworkRequest()}}