学习目标
- 理解协程的基本概念与机制、使用场景
- 了解基本的使用方法
- 本文章不深入原理,只介绍基本概念与使用方法。
协程是什么?
协程是可挂起计算的实例。它在概念上类似于线程,因为它需要一个与其余代码同时工作的代码块来运行。但是,协程不绑定到任何特定线程。它可以在一个线程中暂停执行,并在另一个线程中恢复。
协程可以被认为是轻量级线程,但是存在许多重要的差异,使得它们的实际用法与线程非常不同。 kotlinlang.org/docs/corout…
协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
一个进程可以包含多个线程,一个线程也可以包含多个协程。
简单来说,协程是一种比线程更加轻量的异步任务机制。协程是由用户程序自己实现的,是用户态的自定义抽象,而线程则要依赖于操作系统来实现。
协程与线程该如何选择?
协程与线程相比的优势:
- 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
1、两种任务类型 1.1、计算密集型 计算密集型任务是指在任务执行过程中需要进行大量的计算,较为消耗CPU资源,如复杂的算法模型运算、高清视频解码等,这类任务通常适合使用多进程来提升程序运行效率。
1.2、IO密集型 IO密集型任务特点是CPU消耗少,涉及到网络、磁盘IO的任务都是IO密集型任务,任务的大部分时间都在等待IO操作完成。以网络爬虫为例,在请求网页时可能会需要一定时间等待目标网站响应,此时可以多线程或协程提升程序运行效率。
2、多进程、多线程、协程使用场景 计算密集型:多进程,可以最大限度发挥CPU运算能力。 IO 密集型:推荐优先使用协程,内存开销少,执行效率高;其次是多线程,虽然不如协程高效,但同样能极大提升程序运行效率。 CPU 密集和 IO 密集:多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 原文:blog.csdn.net/qq_38017966…
协程使用依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"
如何创建协程?
创建一个协程有三种方式:
// 1.
@Throws(InterruptedException::class)
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T
// 2.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
// 3.
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
// 使用
private val coroutineScope = CoroutineScope(Dispatchers.Default)
runBlocking() {
// ……
}
val job = coroutineScope.launch(context = Dispatchers.IO) {
// ……
}
val async = coroutineScope.async {
// ……
}
协程遵循结构化并发原则,这意味着新的协程只能在限定协程生命周期的特定CoroutineScope中启动。
什么是结构化并发? 多线程的并发是全局的,而结构化并发中,每个并发都有自己作用域。
在结构化的并发中,每个并发操作都有自己的作用域,并且:
在父作用域内新建作用域都属于它的子作用域;
父作用域和子作用域具有级联关系;
父作用域的生命周期持续到所有子作用域执行完;
当主动结束父作用域时,会级联结束它的各个子作用域。 www.jianshu.com/p/3dc8ba43c…
CoroutineScope 协程作用域
定义新协程的作用范围。每个协程构建器(如launch、async等)都是作用域的扩展,继承作用域的上下文环境,自动传播其所有元素和取消事件。
CoroutineContext 上下文环境
协程的持久上下文,指定协程运行的线程环境, Dispatchers.IO、Dispatchers.IO、Dispatchers.Default、Dispatchers.Main、Dispatchers.Unconfined。创建协程时,如果未指定,则默认采用所属所属作用域的上下文环境.
CoroutineStart 启动参数
协程的启动参数,DEFAULT:根据上下文立即调度协程执行;LAZY:延迟启动协程,仅在需要时启动;即调用start、join、await时才开始执行。ATOMIC:和DEFAULT类似,且在第一个挂起点前不能被取消UNDISPATCHED:立即在当前线程执行协程体,直到遇到第一个挂起点(后面取决于调度器)
深入理解CoroutineStart启动模式
runBlocking
以阻塞的方式运行一个新的协程。该方法不应该在协程中使用,它旨在将常规阻塞代码连接到以挂起样式编写的库,主要用于main 函数和 test 中。 如果该阻塞的线程被中断,则该协程被取消,并抛出异常InterruptedException
CoroutineScope.launch
在不阻塞当前线程的情况下启动一个新的协程,并将对协程的引用作为 Job 返回,可用来操作协程和获取协程执行状态。
如果协程出现未捕获的异常,则整个作用域都将会被取消。 意思是,如果有一个协程抛出异常,则该作用域下的所有协程都会被取消。除非你指定CoroutineExceptionHandler
CoroutineScope.async
与launch的区别在于,该方法返回Deferred。
Job 后台作业
该对象是已启动协程的句柄,可用来对协程进行操作:获取携程执行状态,取消协程,挂起协程,开始协程等。
Job的状态机制
Deferred 带有返回值的Job
Deferred value is a non-blocking cancellable future — it is a Job with a result.
相当于一个带有返回值的Job,该对象继承Job。Deferred具有与 Job相同的状态机制,并具有额外的便捷方法来检索已执行的计算的成功或失败结果(getCompleted、getCompletionExceptionOrNull)。
具有await方法,该方法是一个挂起函数,可以在协程执行完成之后检索结果,如果协程执行失败,则抛出异常。需要注意的是,取消的协程,也被视为完成。
如何取消协程?
需要注意的是,协程并不直接支持 cacel操作,要使cacel有两种方式:
- 主动检查协程状态
- 调用挂起函数,挂起函数会自动检查协程状态。
There are two approaches to making computation code cancellable. The first one is to **periodically invoke a suspending function that checks for cancellation. **There is a yield function that is a good choice for that purpose. The other one is to explicitly check the cancellation status.
suspend fun makeTheCoroutineCancelable() {
val launch = coroutineScope.launch {
var nextPrintTime = System.currentTimeMillis()
var i = 0
while (true) {
// 取消协程方式 1 调用一个挂起函数
yield()
// 取消协程方式 2 主动检查状态
// if (!this.isActive) {
// println("job: I'm canceling ... isActive ${this.isActive}")
// return@launch
// }
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm running ${i++} ... isActive ${this.isActive}")
nextPrintTime += 500L
}
}
}
delay(1000 * 3L)
launch.cancel()
}
协程创建后,会返回一个Job/Deferred对象, 我们可以通过该对象来操作协程。常见操作如下:cancel:取消协程,类似: cancelAndJoin cancelChildren join:暂停当前协程,直到该Job完成。start:使协程开始执行。
另外一点,协程在取消时,会抛出一个异常CancellationException,这个异常是线程取消的正常机制,不会在控制台打印,也不需要特殊处理。除非你需要监控线程取消的情况,可以使用try cache来捕获该异常。
该异常不会影响其他协程的运行,也不会引起程序崩溃。
如何在协程中运行不可取消的代码?
想要在协程中运行不可取消的代码块,可以使用withContext。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
withContext使用给定的协程上下文调用指定的挂起块,挂起直到完成,并返回结果。该方法可用于在协程中切换上下文而不用新开协程。
正常情况下,如果该挂起块所在的协程被取消,则该挂起块的执行也会被取消。仅当withContext(NonCancellable) { ... }时,该挂起块时不可取消的。
fun testWithContext() {
val launch = coroutineScope.launch() {
try {
// 这是一个挂起函数,
val result1 = withContext(Dispatchers.IO) {
delay(1000 * 1L)
println(" this context is $this.coroutineContext")
return@withContext 22
}
println("result1 is $result1")
delay(1000 * 19L)
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 测试不可取消的挂起块
withContext(NonCancellable) {
delay(1000 * 7L)
println(" this context is $this.coroutineContext")
}
}
}
coroutineScope.launch {
delay(1000L * 2)
launch.cancel()
}
}
this context is DispatchedCoroutine{Active}@768c4ba.coroutineContext
result1 is 22
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@c39e645
this context is UndispatchedCoroutine{Active}@4396571c.coroutineContext
如何监控协程执行时间?
使用withTimeout方法,可以指定代码运行时间,如果超时,则会取消代码块内的执行,并抛出TimeoutCancellationException。TimeoutCancellationException继承CancellationException ,也是协程正常结束的机制 。
withTimeout中的超时事件相对于在其块中运行的代码是异步的,随时可能发生, 如果在超时块内有打开资源的情况,最好捕获该异常,在**finally**中关闭。
兄弟函数withTimeoutOrNull,在超时时不会抛出异常,而是返回null。
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T
public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T?
fun testWithTimeout() {
coroutineScope.launch {
// TimeoutCancellationException继承CancellationException 默认不会打印到控制台
withTimeout(1000) {
delay(1000 * 3L)
}
println("该协程因为超时而取消,所以并不会执行这行打印")
}
// 上面的协程在超时之后,停止之后,并不会影响这个协程的执行
coroutineScope.launch {
println("前面的协程在超时之后,并不会影响这一个协程的执行")
delay(1000 * 3L)
try {
withTimeout(1000) {
delay(1000 * 3L)
}
} catch (e: Exception) {
e.printStackTrace()
}
val result = withTimeoutOrNull(1000) {
delay(1000 * 3L)
}
println("result is $result")
}
}
前面的协程在超时之后,并不会影响这一个协程的执行
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:502)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:108)
at java.base/java.lang.Thread.run(Thread.java:829)
result is null
协程中如何处理异常?
协程的异常处理有两种方式:
- 直接在代码块外面使用
try cache - 设置统一的
CoroutineExceptionHandler。有意思的是,CoroutineExceptionHandler居然是继承CoroutineContext。是否能捕获到线程取消事件。- 该处理虽然能捕获异常,但是会取消作用域内协程的执行。
- 该处理不能捕获
CancellationException。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("exceptionHandler,异常拦截 $coroutineContext 出现异常 ${throwable.message}")
}
private val coroutineScope = CoroutineScope(Dispatchers.Default + exceptionHandler)
fun testException() {
coroutineScope.launch() {
delay(1000)
throw NullPointerException("异常测试")
}
// 等上面的协程抛出异常后,该协程就会被取消
coroutineScope.launch {
for (i in 1..10000) {
println("launch Default ${this.isActive}")
delay(1000L)
}
}
}
launch Default true
launch Default true
exceptionHandler,异常拦截 [cn.com.hmsm.kotlindem.CoroutinesDemosKt$special$$inlined$CoroutineExceptionHandler$1@13c48760, StandaloneCoroutine{Cancelling}@20dea219, Dispatchers.Default] 出现异常 异常测试
什么是挂起函数?
使用suspend关键字修饰的函数称为挂起函数,挂起函数只能在协程或挂起函数中运行。
“挂起”类似于线程中的“阻塞”。但“挂起”并不会阻塞线程,只是挂起当前协程,去执行函数的内容,函数执行完成之后,重新恢复当前协程。
协程被挂起期间,其他的协程不会受到影响。
其他常用方法?
await:用于获取CoroutineScope.async创建的协程的返回值delay:将协程延迟给定时间而不阻塞线程,并在指定时间后恢复它。yield:如果可能,将当前协程调度程序的线程(或线程池)让给同一调度程序上的其他协程运行。可用在协程中,检验协程是否被取消。如何取消协程?join:暂停协程,直到此作业完成。相当于将协程变成了一个挂起函数。