协程上下文的设计与使用
协程是一种轻量级的并发编程模式,它可以让我们用同步的方式写出异步的代码,提高代码的可读性和性能。在协程框架中,有一个非常重要的概念,就是协程上下文(CoroutineContext)。协程上下文可以看作是一组协程运行所需的环境变量,比如调度器、异常处理器、协程名等。在本文中,我们将介绍协程上下文的设计与使用,以及它在协程中的作用和用法。
什么是协程上下文
协程上下文是一个实现了CoroutineContext接口的对象,它是一个键值对的集合,其中键是CoroutineContext.Key类型,值是CoroutineContext.Element类型。每个键值对代表了一个协程上下文的元素,比如CoroutineDispatcher、Job、CoroutineName和CoroutineExceptionHandler等。每个元素都有一个唯一的键,用于在协程上下文中查找和替换元素。例如,CoroutineDispatcher的键是ContinuationInterceptor.Key,Job的键是Job.Key等。
协程上下文的主要作用是为协程提供运行时所需的信息和功能。例如:
- CoroutineDispatcher决定了协程在哪个线程或线程池中执行,可以实现协程的调度和切换。
- Job表示了协程的生命周期状态和取消机制,可以实现协程的管理和控制。
- CoroutineName为协程提供了一个可读性较高的名字,可以方便地在日志或调试器中识别协程。
- CoroutineExceptionHandler为协程提供了一个统一的异常处理器,可以捕获并处理协程中抛出的未捕获异常。
协程上下文的接口设计
协程上下文的接口设计非常灵活和优雅,它利用了运算符重载来实现不同的功能。
例如:
- 协程上下文可以通过+运算符来合并两个或多个协程上下文,得到一个新的包含所有元素的协程上下文。如果两个协程上下文中有相同键的元素,则后面的元素会覆盖前面的元素。例如:
// 创建一个指定调度器和名字的协程上下文
val context1 = Dispatchers.IO + CoroutineName("IO")
// 创建一个指定异常处理器和名字的协程上下文
val context2 = CoroutineExceptionHandler { _, e -> println(e) } + CoroutineName("EH")
// 合并两个协程上下文,得到一个新的包含所有元素且名字为EH的协程上下文
val context3 = context1 + context2
- 协程上下文可以通过-运算符来移除一个或多个元素,得到一个新的不包含指定元素的协程上下文。如果要移除的元素不存在,则返回原来的协程上下文。例如:
// 创建一个指定调度器、名字和异常处理器的协程上下文
val context = Dispatchers.IO + CoroutineName("IO") + CoroutineExceptionHandler { _, e -> println(e) }
// 移除调度器和名字元素,得到一个新的只包含异常处理器元素的协程上下文
val newContext = context - Dispatchers.IO - CoroutineName("IO")
- 协程上下文可以通过[]运算符来获取指定键对应的元素,如果不存在则返回null。例如:
// 创建一个指定调度器、名字和异常处理器的协程上下文
val context = Dispatchers.IO + CoroutineName("IO") + CoroutineExceptionHandler { _, e -> println(e) }
// 获取调度器元素
val dispatcher = context[ContinuationInterceptor.Key]
// 获取名字元素
val name = context[CoroutineName.Key]
// 获取异常处理器元素
val handler = context[CoroutineExceptionHandler.Key]
- 协程上下文可以通过fold函数来遍历所有的元素,并对每个元素进行操作,得到一个累积的结果。例如:
// 创建一个指定调度器、名字和异常处理器的协程上下文
val context = Dispatchers.IO + CoroutineName("IO") + CoroutineExceptionHandler { _, e -> println(e) }
// 遍历所有的元素,并打印它们的键和值
val result = context.fold("") { acc, element ->
acc + "${element.key}: ${element}\n"
}
println(result)
如何使用协程上下文
协程上下文可以在不同的场景和方法中使用,主要有以下几种:
- 协程作用域(Coroutine Scope):
协程作用域是一个实现了CoroutineScope接口的对象,它可以用来创建和管理协程。协程作用域有一个coroutineContext属性,表示该作用域内创建的所有协程共享的协程上下文。我们可以通过构造函数或扩展函数来创建自定义的协程作用域,并指定其协程上下文。例如:
// 创建一个指定调度器和名字的协程作用域
val scope1 = CoroutineScope(Dispatchers.IO + CoroutineName("IO"))
// 创建一个指定异常处理器和名字的协程作用域
val scope2 = CoroutineScope(CoroutineExceptionHandler { _, e -> println(e) } + CoroutineName("EH"))
// 在两个不同的协程作用域中启动两个不同的协程
scope1.launch {
// 这个协程会在IO线程中执行,并且名字为IO
println(Thread.currentThread().name)
}
scope2.launch {
// 这个协程会在默认线程中执行,并且名字为EH,如果抛出异常,则会被异常处理器捕获并打印
println(Thread.currentThread().name)
throw RuntimeException("Oops!")
}
- 协程启动函数(Coroutine Start Function):协程启动函数是一些用来创建和启动协程的函数,比如launch、async、runBlocking等。这些函数都有一个可选的参数,用来指定创建的协程的协程上下文。如果没有指定,则使用默认的或父级的协程上下文。例如:
// 在全局作用域中启动一个指定调度器和名字的协程
GlobalScope.launch(Dispatchers.IO + CoroutineName("IO")) {
// 这个协程会在IO线程中执行,并且名字为IO
println(Thread.currentThread().name)
}
// 在主线程中阻塞地启动一个指定异常处理器和名字的协程
runBlocking(CoroutineExceptionHandler { _, e -> println(e) } + CoroutineName("RB")) {
// 这个协程会在主线程中执行,并且名字为RB,如果抛出异常,则会被异常处理器捕获并打印
println(Thread.currentThread().name)
throw RuntimeException("Oops!")
}
- 协程切换函数(Coroutine Switch Function):协程切换函数是一些用来在不同的调度器或线程中切换执行协程的函数,比如withContext、suspendCoroutine等。这些函数都有一个必须的参数,用来指定切换到的目标调度器或线程。这些函数会暂停当前的协程,并在目标调度器或线程中恢复执行,同时保留其他的协程上下文元素。例如:
// 在全局作用域中启动一个指定名字和异常处理器的协程
GlobalScope.launch(CoroutineName("Main") + CoroutineExceptionHandler { _, e -> println(e) }) {
// 这个协程会在默认线程中执行,并且名字为Main,如果抛出异常,则会被异常处理器捕获并打印
println(Thread.currentThread().name)
// 使用withContext函数切换到IO线程中执行
withContext(Dispatchers.IO) {
// 这个协程会在IO线程中执行,但是仍然保留名字为Main和异常处理器
println(Thread.currentThread().name)
// 使用suspendCoroutine函数切换到一个自定义的线程中执行
suspendCoroutine { continuation ->
// 创建一个自定义的线程
val thread = Thread {
// 在自定义的线程中恢复协程的执行,但是仍然保留名字为Main和异常处理器
println(Thread.currentThread().name)
continuation.resume(Unit)
}
thread.start()
}
}
}
- coroutineContext属性(coroutineContext Property):coroutineContext属性是一个挂起函数,它可以在任何协程中调用,用来获取当前协程的协程上下文。我们可以通过这个属性来访问或修改协程上下文中的元素。例如:
// 在全局作用域中启动一个指定名字的协程
GlobalScope.launch(CoroutineName("Main")) {
// 获取当前协程的协程上下文
val context = coroutineContext
// 获取当前协程的名字元素
val name = context[CoroutineName.Key]
// 打印当前协程的名字
println(name)
// 修改当前协程的名字元素
coroutineContext[CoroutineName.Key] = CoroutineName("New")
// 打印修改后的协程的名字
println(coroutineContext[CoroutineName.Key])
}
协程上下文的内置类型
协程上下文中有一些常见的元素类型,它们都是CoroutineContext.Element的子类,实现了不同的功能。我们可以通过构造函数或工厂函数来创建这些元素,并添加到协程上下文中。以下是一些常见的元素类型:
- CoroutineDispatcher:CoroutineDispatcher是一个抽象类,它表示了一个调度器,用来决定协程在哪个线程或线程池中执行。CoroutineDispatcher有一些内置的实现,比如Dispatchers.Default、Dispatchers.IO、Dispatchers.Main等,分别表示了默认线程池、IO线程池和主线程。我们也可以通过asCoroutineDispatcher函数将一个ExecutorService转换为一个CoroutineDispatcher,或者通过newSingleThreadContext函数创建一个单线程的CoroutineDispatcher。例如:
// 创建一个指定调度器为IO线程池的协程上下文
val context1 = Dispatchers.IO
// 创建一个指定调度器为主线程的协程上下文
val context2 = Dispatchers.Main
// 创建一个指定调度器为自定义线程池的协程上下文
val executor = Executors.newFixedThreadPool(4)
val context3 = executor.asCoroutineDispatcher()
// 创建一个指定调度器为单线程的协程上下文
val context4 = newSingleThreadContext("MyThread")
- Job:
Job是一个接口,它表示了一个协程的生命周期状态和取消机制。Job有一些内置的实现,比如SupervisorJob、Deferred等,分别表示了一个监督任务和一个有返回值的任务。我们也可以通过Job函数创建一个普通的Job对象。Job有以下几个特点:
- Job可以有父子关系,当一个父Job被取消时,它的所有子Job也会被取消;当一个子Job抛出异常时,它会传播给父Job,除非父Job是SupervisorJob。
- Job可以有状态,包括New、Active、Completing、Completed、Cancelling和Cancelled等,表示了不同阶段的任务。
- Job可以被取消,通过cancel函数或cancelAndJoin函数来取消一个任务,并等待它完成;通过join函数来等待一个任务完成;通过isActive、isCompleted和isCancelled属性来检查任务是否处于某种状态。
- Job可以注册回调函数,通过invokeOnCompletion函数或onJoin函数来在任务完成时执行一些操作;通过cancelChildren函数或cancelChildrenAndJoin函数来取消所有子任务,并等待它们完成。
// 创建一个指定任务为普通任务的协程上下文
val context1 = Job()
// 创建一个指定任务为监督任务的协程上下文
val context2 = SupervisorJob()
// 在全局作用域中启动一个指定任务为有返回值的协程
val deferred = GlobalScope.async {
// 这个协程会返回一个结果
42
}
// 获取协程的任务元素
val job = deferred
// 等待协程完成,并获取结果
val result = deferred.await()
- CoroutineName:CoroutineName是一个数据类,它表示了一个协程的名字,用于在日志或调试器中识别协程。我们可以通过CoroutineName函数创建一个CoroutineName对象,并指定一个字符串作为名字。例如:
// 创建一个指定名字为Main的协程上下文
val context = CoroutineName("Main")
// 在全局作用域中启动一个指定名字的协程
GlobalScope.launch(CoroutineName("IO")) {
// 这个协程的名字为IO
println(coroutineContext[CoroutineName.Key])
}
- CoroutineExceptionHandler:CoroutineExceptionHandler是一个函数式接口,它表示了一个协程的异常处理器,用于捕获并处理协程中抛出的未捕获异常。我们可以通过CoroutineExceptionHandler函数创建一个CoroutineExceptionHandler对象,并指定一个函数作为异常处理逻辑。例如:
// 创建一个指定异常处理器为打印异常的协程上下文
val context = CoroutineExceptionHandler { _, e ->
println(e)
}
// 在全局作用域中启动一个指定异常处理器的协程
GlobalScope.launch(context) {
// 这个协程如果抛出异常,则会被异常处理器捕获并打印
throw RuntimeException("Oops!")
}
标签:协程,CoroutineName,val,线程,IO,设计,上下文 From: https://blog.51cto.com/u_16175630/7086267