Kotlin 协程几个核心概念
Kotlin 协程几个核心概念
Kotlin 协程的使用主要围绕几个核心概念展开:协程构建器、挂起函数、协程上下文(包括调度器),以及协程作用域。下面将详细介绍这些概念及其使用方法
协程构建器
Kotlin 协程提供了几个构建器来创建和启动协程,最常用的有 launch 和 async。
launch 构建器用于在协程作用域中启动一个新的协程。它返回一个 Job 对象,可以用来管理协程的生命周期(例如取消协程)。
private fun test_coroutine() { runBlocking { // 这里的 runBlocking 创建一个新的协程作用域 val job = launch { // 在启动一个新的协程并继续 delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) debug("World!") } debug("Hello,") // 协程已在等待时主线程还在继续 job.join() // 等待直到子协程执行结束 } }async 与 launch 类似,但它返回一个 Deferred 对象,这是一个轻量级的非阻塞的 future,表示一个可能在未来完成的异步计算。Deferred 对象可以通过 .await() 方法来获取计算的结果。
private fun test_coroutine() { runBlocking { // 这里的 runBlocking 创建一个新的协程作用域 val deferred = async { // 启动一个新的协程并继续 delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) return@async "Hello, World!" } debug("Waiting for result...") // 协程已在等待时主线程还在继续 debug(deferred.await()) } }挂起函数
挂起函数是可以在协程中挂起执行而不阻塞线程的特殊函数,用 suspend 关键字标记。
suspend fun doSomething() { delay(1000L) // 模拟耗时操作 println("Done")}挂起函数与状态机
Kotlin 编译器将使用协程的代码转换成状态机的形式。这一过程主要涉及到挂起函数的调用。
- 挂起函数:Kotlin 中的挂起函数通过 suspend 关键字标识。这类函数只能在协程中或其他挂起函数中调用。挂起函数在调用点可以挂起协程的执行,不阻塞线程,并在挂起操作完成后恢复协程的执行。
- 状态机转换:编译器将含有挂起点的协程代码转换成一个状态机。每个挂起点都对应状态机中的一个状态。编译器为协程生成的代码负责在挂起点之间转移,并在适当的时候恢复执行。
挂起与恢复的实现
挂起和恢复的实现是通过 Continuation(续体)来完成的。每个挂起函数都接受一个 Continuation 参数,这个参数包含了协程在挂起点之后继续执行的所有信息。
- Continuation:续体是协程挂起和恢复的关键。它持有协程的上下文信息,包括协程在挂起时的状态和恢复执行时应当执行的代码位置。
- 回调的转换:挂起函数通常会将异步操作转换为同步操作的形式。这是通过将 Continuation 传递给异步操作,然后在异步操作完成时调用 Continuation 的 resume 或 resumeWithException 方法来实现的。
协程上下文
协程上下文提供了关于协程的运行信息,当创建新的协程时,可以使用 CoroutineContext 来指定协程的运行环境,CoroutineContext 定义协程行为的一组元素,每个元素都有一个唯一的键。它由以下部分:
- Job — 控制协程的生命周期。
- CoroutineDispatcher — 将工作分派到适当的线程。
- CoroutineName — 协程的名称,对于调试很有用。
- CoroutineExceptionHandler — 处理未捕获的异常。
调度器
协程上下文中包含调度器,调度器决定了协程在哪个线程或线程池上执行。
- Dispatchers.Main:在 Android 中,这个调度器用于在主线程上执行操作。
- Dispatchers.IO:适用于磁盘和网络 I/O 操作,使用共享的后台线程池。
- Dispatchers.Default:适用于计算密集型任务,使用共享的后台线程池。
launch(Dispatchers.IO) { // 在 I/O 调度器上执行} // 例如 runBlocking(Dispatchers.IO) { 这里的 runBlocking 创建一个新的协程作用域 val deferred = async { // 在后台启动一个新的协程并继续 delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) return@async "Hello, World!" } debug("Waiting for result...") // 协程已在等待时主线程还在继续 debug(deferred.await()) }协程作用域
常见作用域
在 Kotlin 协程中,协程作用域(Coroutine Scope)是一个非常重要的概念,用于控制协程的生命周期。协程作用域提供了一个上下文环境,允许你启动新的协程,同时保证所有启动的协程都能在合适的时间被取消或完成。以下是一些常用的协程作用域:
GlobalScope
- 用途:在全局范围内启动协程,其生命周期与应用程序的生命周期一致。
- 特点:启动的协程会在整个应用程序中持续运行,直到协程完成或应用程序终止。
- 注意:由于 GlobalScope 协程的生命周期过长,可能会导致内存泄漏等问题。因此,建议在特定作用域内管理协程。
coroutineScope
- 用途:自定义作用域,可以根据需要创建。
- 特点:通过 CoroutineScope 创建的作用域,可以绑定到应用程序的组件或对象的生命周期,如 Android 的 Activity 或 Fragment。
- 示例:自定义作用域通常与结构化并发一起使用,确保相关协程在特定组件的生命周期结束时一起取消。
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() { override fun onDestroy() { super.onDestroy() cancel() // 取消由该 Activity 启动的所有协程 }}viewModelScope
- 用途:在 Android 的 ViewModel 中使用,生命周期绑定到 ViewModel。
- 特点:当 ViewModel 被清除时,viewModelScope 会自动取消其启动的所有协程,避免内存泄漏。
- 示例:在 ViewModel 中使用 viewModelScope 启动协程处理数据加载。
runBlocking
- 用途:创建一个新的协程作用域,并阻塞当前线程直到所有协程完成。
- 特点:主要用于桥接阻塞代码和非阻塞的协程世界,例如在 main 函数或测试中。
- 注意:runBlocking 旨在主线程中短暂使用,不应在生产环境的代码中频繁使用,以避免阻塞主线程。
这些协程作用域的选择和使用取决于你的应用程序的具体需求以及你想要的协程的生命周期管理方式。在开发过程中,选择合适的协程作用域对于确保资源正确管理和避免内存泄漏至关重要。
协程上下文的继承关系
由于 CoroutineScope 可以创建协程,并且您可以在协程内创建更多协程,因此会创建隐式任务层次结构。在任务层次结构中,每个协程都有一个父级,它可以是 CoroutineScope 也可以是另一个协程。
Kotlin 协程中,CoroutineContext 使用了继承的概念,其中一个上下文可以继承另一个上下文的属性。这种继承关系使得在创建新协程时可以继承父协程的上下文,从而共享父协程的属性。
下面是一个简单的示例,演示了在 Kotlin 协程中如何使用 CoroutineContext 继承:
@Test fun test_CoroutineContext() { val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } runBlocking { val parentCoroutineContext=Job() + Dispatchers.Default + CoroutineName("Parent") + handler val scope = CoroutineScope(parentCoroutineContext) println("-Job:" + scope.coroutineContext[Job]) println("-ContinuationInterceptor:" + scope.coroutineContext[ContinuationInterceptor]) println("-CoroutineName:" + scope.coroutineContext[CoroutineName]) println("-CoroutineExceptionHandler:" + scope.coroutineContext[CoroutineExceptionHandler]) val jobA = scope.launch(Dispatchers.IO + CoroutineName("A")) { println("--Job:" + this.coroutineContext[Job]) println("--ContinuationInterceptor:" + this.coroutineContext[ContinuationInterceptor]) println("--CoroutineName:" + this.coroutineContext[CoroutineName]) println("--CoroutineExceptionHandler:" + this.coroutineContext[CoroutineExceptionHandler]) val jobA1 = launch { println("---Job:" + this.coroutineContext[Job]) println("---ContinuationInterceptor:" + this.coroutineContext[ContinuationInterceptor]) println("---CoroutineName:" + this.coroutineContext[CoroutineName]) println("---CoroutineExceptionHandler:" + this.coroutineContext[CoroutineExceptionHandler]) } delay(1000) println("A task end ") } val jobB = scope.launch() { println("-- B Job:" + this.coroutineContext[Job]) println("-- B ContinuationInterceptor:" + this.coroutineContext[ContinuationInterceptor]) println("-- B CoroutineName:" + this.coroutineContext[CoroutineName]) println("-- B CoroutineExceptionHandler:" + this.coroutineContext[CoroutineExceptionHandler]) delay(1000) println("B task end") } } }======> [10:33:28.068][Test worker @coroutine#1](ExampleUnitTest_CoroutineInherited.kt:41) -Job:JobImpl{Active}@7fc4780b======> [10:33:28.070][Test worker @coroutine#1](ExampleUnitTest_CoroutineInherited.kt:42) -ContinuationInterceptor:Dispatchers.Default======> [10:33:28.070][Test worker @coroutine#1](ExampleUnitTest_CoroutineInherited.kt:43) -CoroutineName:CoroutineName(Parent)======> [10:33:28.070][Test worker @coroutine#1](ExampleUnitTest_CoroutineInherited.kt:44) -CoroutineExceptionHandler:com.dafay.demo.coroutine.ExampleUnitTest_CoroutineInherited$test_CoroutineContext$$inlined$CoroutineExceptionHandler$1@57ad2aa7======> [10:33:28.076][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineInherited.kt:46) --Job:"A#2":StandaloneCoroutine{Active}@520f37a7======> [10:33:28.076][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineInherited.kt:47) --ContinuationInterceptor:Dispatchers.IO======> [10:33:28.076][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineInherited.kt:48) --CoroutineName:CoroutineName(A)======> [10:33:28.076][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineInherited.kt:49) --CoroutineExceptionHandler:com.dafay.demo.coroutine.ExampleUnitTest_CoroutineInherited$test_CoroutineContext$$inlined$CoroutineExceptionHandler$1@57ad2aa7======> [10:33:28.077][DefaultDispatcher-worker-3 @A#4](ExampleUnitTest_CoroutineInherited.kt:51) ---Job:"A#4":StandaloneCoroutine{Active}@3d9f9e36======> [10:33:28.077][DefaultDispatcher-worker-3 @A#4](ExampleUnitTest_CoroutineInherited.kt:52) ---ContinuationInterceptor:Dispatchers.IO======> [10:33:28.077][DefaultDispatcher-worker-3 @A#4](ExampleUnitTest_CoroutineInherited.kt:53) ---CoroutineName:CoroutineName(A)======> [10:33:28.078][DefaultDispatcher-worker-3 @A#4](ExampleUnitTest_CoroutineInherited.kt:54) ---CoroutineExceptionHandler:com.dafay.demo.coroutine.ExampleUnitTest_CoroutineInherited$test_CoroutineContext$$inlined$CoroutineExceptionHandler$1@57ad2aa7======> [10:33:28.078][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineInherited.kt:60) -- B Job:"Parent#3":StandaloneCoroutine{Active}@1acade9e======> [10:33:28.078][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineInherited.kt:61) -- B ContinuationInterceptor:Dispatchers.Default======> [10:33:28.078][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineInherited.kt:62) -- B CoroutineName:CoroutineName(Parent)======> [10:33:28.079][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineInherited.kt:63) -- B CoroutineExceptionHandler:com.dafay.demo.coroutine.ExampleUnitTest_CoroutineInherited$test_CoroutineContext$$inlined$CoroutineExceptionHandler$1@57ad2aa7======> [10:33:29.086][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineInherited.kt:57) A task end ======> [10:33:29.086][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineInherited.kt:65) B task end 在这个示例中,我们创建了一个父协程的 CoroutineContext parentCoroutineContext ,并用它来构建两个子协程,可以清晰看到子协程继承了父协程的上下文,包括调度器、协程名、异常捕获器,父子协程的 Job 总是不同的(Job 管理对应协程的生命周期)。通过访问 coroutineContext 属性,我们可以查看子协程的上下文信息。
💡 CoroutineContext 可以使用 + 运算符进行组合。由于 CoroutineContext 是一组元素,因此将创建一个新的 CoroutineContext,其中加号右侧的元素将覆盖左侧的元素。例如: (Dispatchers.Main, “name”) +(Dispatchers.IO) = (Dispatchers.IO, “name”)
通过示例,新协程上下文可依据以下公式计算:
新协程上下文 = 父 CoroutineContext + Job()