跳到主要内容

协程的基础知识(二)

一 结构化并发

什么是结构化并发,简单来说就是带有结构和层级的并发,这里的结构和层次指的就是协程的父子关系,而并发操作指的就是前一篇文章我们所说的cancel等操作。(简单理解:结构化并发就是协程的父子关系)

1.1 父子协程

信息

子协程的生命周期受父协程的影响,父协程取消,子协程也会取消 当一个协程被其它协程在 CoroutineScope 中启动的时候, 它将通过 CoroutineScope.coroutineContext 来承袭上下文,并且这个新协程的 Job 将会成为父协程作业的 子 作业。当一个父协程被取消的时候,所有它的子协程也会被递归的取消。

  • 案例
package model

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* @description:
* @author: shu
* @createDate: 2023/8/14 9:34
* @version: 1.0
*/

// 结构化并发
fun main() = runBlocking {
val parentJob = launch {
val child1 = launch {
println("子协程1执行")
delay(1000)
println("子协程1完成")
}

val child2 = launch {
println("子协程2执行")
delay(1500)
println("子协程2完成")
}

// 等待所有子协程完成
joinAll(child1, child2)
println("所有子协程完成")
}

parentJob.join()
println("父协程完成")
}

// 测试
fun main() {
println("main: Now I can quit.")
main
}


  • 从源码的角度看

image-20230814093938915

可以看到每个Job对象都会有一个children属性,它的类型是Sequence,是一个惰性集合

在 Kotlin 协程中,如果你在一个协程内部使用 launchasync 函数来创建新的协程,那么新的协程会成为当前协程的子协程。这种关系会在协程的取消操作中起作用。当父协程被取消时,所有的子协程也会被自动取消,从而避免内存泄漏和资源浪费。

二 关键字

2.1 suspend(挂起函数)

信息
  • 挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起了。协程中调用挂起函数时,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了。

  • 也就是说,协程内挂起函数之后的代码停止执行了,直到挂起函数完成后恢复协程,协程才继续执行后续的代码。所有挂起函数都会通过suspend修饰符修饰。

suspend 关键字是 Kotlin 编程语言中的一个重要概念。由于您的用户配置文件是 "你是Java代码辅助开发",因此我假设您对 Java 有一些了解。suspend 关键字用于在 Kotlin 协程中声明挂起函数,这是一种异步编程的技术,类似于 Java 中的多线程。Kotlin 协程允许您以一种更加顺畅和可读性更高的方式处理异步操作。

在 Kotlin 中,当您在函数声明前加上 suspend 关键字时,您就创建了一个挂起函数。挂起函数可以在不阻塞主线程的情况下暂停执行,等待一些耗时的操作完成。这使得编写异步代码更加容易,而不需要显式地创建线程或处理复杂的回调。

以下是一个简单的示例,展示了如何在 Kotlin 中使用 suspend 关键字来定义一个挂起函数:

import kotlinx.coroutines.*

suspend fun fetchDataFromNetwork(): String {
delay(1000) // 模拟耗时操作
return "Data from network"
}

fun main() {
// 启动一个协程
GlobalScope.launch {
println("Fetching data...")
val data = fetchDataFromNetwork()
println("Data received: $data")
}

// 阻塞主线程,以便协程有足够的时间执行
Thread.sleep(2000)
}

在上面的例子中,fetchDataFromNetwork 函数使用 suspend 关键字声明,允许它在内部使用 delay 函数来模拟耗时的网络请求。协程可以通过 launch 函数在后台执行,而主线程则不会被阻塞。

总而言之,suspend 关键字是 Kotlin 协程中的一个关键概念,用于声明挂起函数,以实现更加顺畅的异步编程。

三 协程上下文与调度器

3.1 协程上下文

信息

协程总是运行在一些以 CoroutineContext 类型为代表的上下文中,它们被定义在了 Kotlin 的标准库里。 协程上下文是各种不同元素的集合。其中主元素是协程中的 Job, 我们在前面的文档中见过它以及它的调度器,而本文将对它进行介绍。

协程的 CoroutineContext 是一个上下文对象,它持有协程的各种属性和元素。在 Kotlin 协程中,CoroutineContext 通常由多个元素组成,这些元素可以包括调度器(用于指定协程运行的线程或线程池)、异常处理器、协程名称等。CoroutineContext 在协程的生命周期中起着重要的作用,它会在协程的不同操作中传播和继承。

在协程代码中,你可以使用 coroutineContext 属性来获取当前协程的 CoroutineContext。例如:

import kotlinx.coroutines.*

fun main() = runBlocking {
val context: CoroutineContext = coroutineContext
println(context)
}

在上述代码中,coroutineContext 返回了当前协程的 CoroutineContext,它可能包含调度器、异常处理器等信息。

另外,你还可以使用 plusminus 操作符来创建新的 CoroutineContext,以便在协程间传递不同的属性。例如:

import kotlinx.coroutines.*

fun main() = runBlocking {
val customContext = coroutineContext + Dispatchers.IO
launch(customContext) {
// 这个协程将在 IO 线程上运行
println("Running on ${Thread.currentThread().name}")
}
}

在这个例子中,我们通过 + Dispatchers.IO 将一个新的调度器添加到协程的 CoroutineContext 中,使得这个协程在 IO 线程上运行。

3.2 协程调度器

信息

协程调度器用于确定协程在哪个线程或线程池中运行。协程调度器可以通过 CoroutineContext 的扩展属性来获取,例如 coroutineContext[ContinuationInterceptor]

public actual object Dispatchers {

// 默认调度器
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
// UI调度器
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
// 无限制调度器
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
// IO调度器
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}

Dispatchers中提供了4种类型调度器:

实现类型具体调度器解释
Default线 程 池DefaultScheduler/CommonPool协程内部实现的Excutor线程池,核心线程数和最大线程数依赖CPU数量,适用于计算类耗时任务调度,比如逻辑计算
MainUI线程MainCoroutineDispatcher通过MainLooper的handler来向主线程派发任务到主线程执行
Unconfined直接执行Unconfined无指定派发线程,会根据运行时的上下文环境决定。
IO线程池LimitingDispatcherIO和Default共享线程池,但运行并发数不同,支持最大并行任务数,适用IO任务调度,比如读写文件,网络请求等
import kotlinx.coroutines.*

fun main() = runBlocking {
launch {
// 运行在父协程的上下文中,即 runBlocking 主协程
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
// 不受限的——将工作在主线程中
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
// 将会获取默认调度器
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {
// 将使它获得一个新的线程
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}

在上面的例子中,我们通过 launch 函数创建了 4 个协程,并分别指定了不同的调度器。其中,Unconfined 调度器将会在主线程中运行,而 Default 调度器将会在后台创建一个新的线程来运行。最后,newSingleThreadContext 调度器将会创建一个新的线程来运行协程。

3.3 协程作用域

信息

CoroutineScope是什么?如果你觉得陌生,那么GlobalScope、lifecycleScope与viewModelScope相信就很熟悉了吧。它们都实现了CoroutineScope接口。

launch()CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围。launch方法有三个参数:1.协程下上文CoroutineContext;2.协程启动模式CoroutineStart;3.协程体: block是一个带接收者的函数字面量,接收者是CoroutineScope

作用域意义解释
CoroutineScope创建协程的局部作用域局部协程作用域,可以指定派发器Dispatcher,并且通过调用返回的Job对象的cancel()方法,可以取消该scope下所有正在进行的任务
GlobalScope创建协程的全局作用域该API启动的协程为顶层协程,是个单例协程作用域,没有父任务,且该scope没有Job对象,所以无法对整个scope执行cancel()操作,所以需要手动管理内部的每个协程
MainScope创建协程的局部作用域,且指定Dispatcher.MainCoroutineScope和Dispatcher.Main组合的协程作用域,会指定派发到Dispatcher.Main中执行,可以通过调用返回的Job对象管理协程
runBlocking创建协程的局部作用域,且阻塞协程所在线程等待结果局部协程作用域,可以指定派发器Dispatcher,会阻塞调用者所在的线程,直到协程内部返回结果。且该scope没有返回Job对象用于协程管理