协程

什么是协程

其实可以把它理解成一种轻量级的线程,但是线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换,使用协程可以仅在编程语言的层面就能实现不同协程的切换,从而大大提升并发编程的运行效率。

我们可以通过协程的官方文档来学习协程:
kotlinlang.org/docs/corout...

第一个协程

Kotlin 并没有将协程纳入标准库的 API 当中,所以我们需要在 build.gradle 中添加如下依赖:

arduino 复制代码
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' 
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'

如果是纯 Kotlin 程序,可以只添加 coroutines-core 就行了。如果是用于 Android 平台的话,可以只添加 coroutines-android,它里面已经包含了 coroutines-core。

接下来创建一个 CoroutinesTest.kt

kt 复制代码
fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second 
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}

打印如下:

Hello 
World!

下面我们分析一下这段代码

launch函数:这个函数用于启动一个新的协程,launch 函数之外的代码可以继续执行,所以上面的例子中 Hello 会先打印,launch 函数中的代码与其他代码是并行的关系。launch 函数的源码如下:

kt 复制代码
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
    return coroutine
}

可以看到 launch 函数是 CoroutineScope 的扩展函数,其中 block 函数是一个挂起函数,扩展了 CoroutineScope 类,这样 launch 函数中的代码就是直接在 CoroutineScope 的上下文中执行的。launch 函数最后返回了 coroutine,表示它是一个协程建造者。

delay函数:delay 函数的源码如下:

kt 复制代码
public suspend fun delay(timeMillis: Long) {
    ...
}

delay 函数前面有 suspend 修饰,表示它是一个挂起函数,意味着它可以挂起当前协程一段时间,挂起协程不会阻塞其所在的线程,它允许其他协程继续执行。

runBlocking函数 :它也是一个协程建造者,它把非协程的代码(比如上例中的 fun main())与协程的代码(比如上例中的 runBlocking{...})连接起来,IDE 中会在 runBlocking 大括号的开头高亮显示this: CoroutineScope,表示这里是 CoroutineScope 的范围。如果在上例中你忘了加 runBlocking,代码会报错,因为 launch 函数是 CoroutineScope 的扩展函数,它只能在 CoroutineScope 中执行。

runBlocking 函数的源码如下:

kt 复制代码
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    ...
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

runBlocking 函数创建的协程会阻塞当前线程直到协程执行结束,runBlocking 函数通常只应该在测试环境下使用,因为线程是宝贵的资源,阻塞线程是不明智的。

随着 launch 函数中的代码逻辑越来越复杂,你可能需要把其中的代码放到一个单独的函数中,如果你试图在一个普通的函数中调用 delay() 函数,IDE 会提示错误:Suspend function 'delay' should be called only from a coroutine or another suspend function。意思是 delay() 函数是一个挂起函数,只能由协程或者由其它挂起函数来调用

这时候可以将该函数声明称挂起函数,代码如下:

kt 复制代码
fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() }
    println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

CoroutineScope

CoroutineScope即协程作用域,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,就容易导致代码臃肿,甚至发生内存泄露。结构化并发可以避免这个问题,协程遵循结构化并发的原则,意味着只能在 CoroutineScope 中启动协程。

可以使用下面这些方式来创建协程作用域:

  • GlobalScope,即全局协程作用域,它不会阻塞当前线程,用这种方式启动的协程类似守护线程,也不会阻止进程结束。
  • runBlocking,和 GlobalScope 不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束。
  • coroutineScope,与 runBlocking 不同的是它不会阻塞当前线程,只会阻塞当前协程直到当前协程执行结束。
  • 自定义 CoroutineScope,可用于实现主动控制协程的生命周期,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而避免内存泄露

1.GlobalScope

GlobalScope 不会阻塞其所在线程,所以以下代码中主线程的日志输出会早于 GlobalScope 内部的日志。

kt 复制代码
fun main() {
    log("start")
    GlobalScope.launch {
        launch {
            delay(400)
            log("launch A")
        }
        launch {
            delay(300)
            log("launch B")
        }
        log("GlobalScope")
    }
    log("end")
    Thread.sleep(500)
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

运行后打印如下:

css 复制代码
[main] start
[main] end
[DefaultDispatcher-worker-1] GlobalScope
[DefaultDispatcher-worker-3] launch B
[DefaultDispatcher-worker-3] launch A

GlobalScope 的源码如下:

kt 复制代码
public object GlobalScope : CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

可以看到 GlobalScope 继承自 CoroutineScope。

GlobalScope.launch 会创建一个在整个应用程序生命周期内运行的顶层协程,使用 GlobalScope 启动的协程并不遵循结构化并发的原则,所以在使用过程中很容易意外地造成资源或内存泄漏。如果由于网络慢造成协程延迟执行,它会保持工作状态耗费资源。比如下面这段代码:

kt 复制代码
fun loadConfiguration() {
     GlobalScope.launch {
         val config = fetchConfigFromServer() // 网络请求
         updateConfiguration(config)
     }
}

如果网络很慢,这个协程会持续在后台等待并耗费资源,所以在日常开发中应该谨慎使用 GlobalScope。

2.runBlocking

runBlocking 会阻塞当前线程直到其内部所有相同作用域的协程执行结束。

kt 复制代码
fun main() {
    runBlocking {
        log("launch")
        delay(1500)
        log("launch end")
    }
    log("end")
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

运行后打印如下:

csharp 复制代码
[main] launch
[main] launch end
[main] end

从上面的打印可以看出,这里 runBlocking 函数内部启动的协程是在主线程运行的。

下面我们看看创建多个协程的场景:

kt 复制代码
fun main() {
    log("start")
    runBlocking {
        launch {
            repeat(3) {
                delay(100)
                log("launchA - $it")
            }
        }
        launch {
            repeat(3) {
                delay(100)
                log("launchB - $it")
            }
        }
        GlobalScope.launch {
            repeat(3) {
                delay(120)
                log("GlobalScope - $it")
            }
        }
    }
    log("end")
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

运行后打印如下:

csharp 复制代码
[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end

从打印结果可以看到,runBlocking 没有等 GlobalScope 作用域的协程打印完就结束了,证实了它只会等待相同协程作用域的协程。子协程的日志是交替打印的,通过 launch 函数启动的两个协程都运行在主线程,但是却实现了类似运行两个子线程的效果。由编程语言来决定如何在多个协程之间调度的,调度的过程完全不需要操作系统的参与,这也就使得协程的并发效率会出奇的高。

具体会有多高呢,看看下面的例子:

kt 复制代码
fun main() {
    val start = System.currentTimeMillis()
    var i = 0
    runBlocking {
        repeat(100000){
            launch { println(++i) }
        }
    }
    val end = System.currentTimeMillis()
    println("cost ${end - start} milliseconds")
}

打印如下:

erlang 复制代码
...
99995
99996
99997
99998
99999
100000
cost 357 milliseconds

上面的代码使用 repeat 函数创建了10万个协程,看打印耗时仅357毫秒,可见协程有多高效。试想一下,如果开启的是10万个线程,程序或许已经 OOM 了。

3.coroutineScope

coroutineScope 函数用于创建一个独立的协程作用域,可以保证其作用域内的所有代码和子协程在全部执行完之前,会一直阻塞当前协程,coroutineScope 的源码如下:

kt 复制代码
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

从源码可以看出 coroutineScope 是 CoroutineScope.kt 中的一个挂起函数,coroutineScope 只能在协程或其他挂起函数中调用。

看下面的示例:

kt 复制代码
fun main() {
    runBlocking {
        coroutineScope {
            launch {
                for(i in 1..3){
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope end")
    }
    println("runBlocking end")
}

打印如下:

arduino 复制代码
1
2
3
coroutineScope end
runBlocking end

从打印可以看到,控制台会每隔1秒依次输出数字1到3,然后打印 coroutineScope 函数结尾的日志,最后打印 runBlocking 函数结尾的日志。由此可见,coroutineScope 函数确实将当前协程阻塞住了,如果这里不加 coroutineScope,"coroutineScope end"会先输出。

但是 coroutineScope 函数只会阻塞当前协程,不会影响其他协程,也不会影响任何线程,因此不会影响性能。而 runBlocking 函数会阻塞当前线程,如果你在主线程中调用它,可能会造成界面卡死的情况,所以不太推荐在实际项目中使用。另外,runBlocking 是一个普通函数,而 coroutineScope 是一个挂起函数。

4.自定义 CoroutineScope

GlobalScope.launch 每次创建都是顶层协程,一般不太建议使用。为什么不太建议使用顶层协程呢?主要还是因为它管理起来成本太高了。比如你在 Activity 中使用协程发起一条网络请求,如果服务器还没有来得及响应这条网络请求,你就退出了这个 Activity,这时候你应该取消这条网络请求以避免内存泄漏。那么协程要怎么取消呢?不管是 GlobalScope.launch 还是 launch 函数都会返回一个 Job 对象,可以调用 Job 对象的 cancel() 方法来取消协程,代码如下:

kt 复制代码
val job = GlobalScope.launch {
    // 处理具体的逻辑
}
job.cancel()

但是如果通过这种方式创建的协程有多个,就需要逐个调用 Job 对象的 cancel() 方法,有没有更好的办法呢?

kotlinx.coroutines 提供了 CoroutineScope 来管理多个协程的生命周期,我们可以通过创建与 Activity 生命周期相关联的协程作用域来管理协程的生命周期。CoroutineScope 的实例可以通过 CoroutineScope()MainScope() 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器。MainScope() 的源码如下:

kt 复制代码
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

可以看到 MainScope() 是一个函数,返回的是 ContextScope 的实例。

具体用法看下面示例:

kt 复制代码
class MainActivity : AppCompatActivity() {

    private val mainScope = MainScope()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mainScope.launch {
            log("launch:0")
        }

        mainScope.launch {
            delay(5000)
            log("launch:5")
        }

        mainScope.launch {
            delay(10000)
            log("launch:10")
        }

        mainScope.launch {
            delay(15000)
            log("launch:15")
        }

        mainScope.launch {
            delay(20000)
            log("launch:20")
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        mainScope.cancel()
    }

    private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

}

这里在 onCreate() 中创建了5个协程,只需要在 onDestroy() 方法中调用一次 mainScope.cancel() 即可将所有协程全部取消。

或者,我们可以通过委托模式来让 Activity 实现 CoroutineScope 接口,从而可以在 Activity 内直接启动协程而不必显示地指定它们的上下文,并且在 onDestroy() 中自动取消所有协程

kt 复制代码
class MyActivity : CoroutineScope by CoroutineScope(Dispatchers.Default), AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launch {
            repeat(5) {
                delay(1000L * it)
                log(it)
            }
        }
        log("Activity Created")

    }

    override fun onDestroy() {
        super.onDestroy()
        cancel()
        log("Activity Destroyed")
    }

    private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

}

当你提前退出 Activity,协程就不再输出日志了:

csharp 复制代码
[main] Activity Created
[DefaultDispatcher-worker-1] 0
[DefaultDispatcher-worker-1] 1
[DefaultDispatcher-worker-1] 2
[DefaultDispatcher-worker-1] 3
[main] Activity Destroyed

已取消的作用域无法再创建协程。因此,仅当控制其生命周期的类被销毁时,才应调用 scope.cancel()。例如,使用 viewModelScope 时, ViewModel 会在自身的 onCleared() 方法中自动取消作用域。

CoroutineBuilder

1.launch

launch 函数是 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即 Job 对象。其代码如下:

kt 复制代码
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
    return coroutine
}

launch 函数共包含三个参数:

  1. context:用于指定协程的上下文,默认值为 EmptyCoroutineContext。
  2. start:用于指定协程的启动方式,默认值为 CoroutineStart.DEFAULT,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为 CoroutineStart.LAZY 来实现延迟启动,即懒加载。
  3. block:用于传递协程的执行体,即希望交由协程执行的任务。

2.async

launch 函数的返回值永远是一个 Job 对象,如果我们想要获取协程的执行结果该怎么办?这时候就需要使用 async 函数。async 函数也是 CoroutineScope 的扩展函数,代码如下:

kt 复制代码
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
    return coroutine
}

async 函数会返回 Deferred 对象,如果我们想要获取 async 函数代码块的执行结果,只需要调用 Deferred 对象的 await() 方法即可,代码如下:

kt 复制代码
fun main() {
    runBlocking {
        var result = async {
            5 + 6
        }.await()
        println(result)
    }
}

运行后打印如下:

11

当调用 await() 方法时,如果代码块中的代码还没有执行完,await() 方法会将当前协程阻塞住,直到可以获得 async 函数的执行结果。看下面的代码:

kt 复制代码
fun main() {
    runBlocking {
        val start = System.currentTimeMillis();
        val result1 = async {
            delay(1000)
            5 + 6
        }.await()

        val result2 = async {
            delay(1000)
            4 + 6
        }.await()

        println("result is ${result1 + result2}.")
        val end = System.currentTimeMillis();
        println("cost ${end - start} ms.")

    }
}

运行后打印如下:

erlang 复制代码
result is 21.
cost 2058 ms.

这里连续使用了2个 async 函数来执行任务,并在代码块中调用 delay() 方法进行1秒的延迟。从打印结果可以看出,整段代码运行耗时 2058 毫秒,说明这里两个 async 函数确实是一种串行的关系,也就证实了 await() 方法在 async 函数代码块中的代码执行完之前确实会一直将当前协程阻塞住。

上面的代码是非常低效的,有没有办法提高运行效率同时拿到正确的运行结果呢?把上面的代码修改一下:

kt 复制代码
fun main() {
    runBlocking {
        val start = System.currentTimeMillis();
        val deferred1 = async {
            delay(1000)
            5 + 6
        }

        val deferred2 = async {
            delay(1000)
            4 + 6
        }

        println("result is ${deferred1.await() + deferred2.await()}.")
        val end = System.currentTimeMillis();
        println("cost ${end - start} ms.")

    }
}

现在我们仅在需要用到 async 函数的执行结果的时候才调用 await() 方法,这样两个 async 函数就是并行的关系。运行后打印如下:

erlang 复制代码
result is 21.
cost 1049 ms.

从打印结果可以看到,耗时变成了 1049 毫秒,运行效率的提升是显而易见的。

3.withContext

withContext() 函数是一个挂起函数,大体可以将它理解成 async 函数的一种简化版写法。示例写法如下:

kt 复制代码
fun main() {
    runBlocking {
        var result = withContext(Dispatchers.Default){
            5 + 6
        }
        println(result)
    }
}

在调用 withContext() 函数后,会立即执行代码块中的代码,同时将当前协程阻塞住。代码块中的代码执行完之后,会将最后一行的执行结果作为 withContext() 函数的返回值返回,这样看来与 async 函数的用法差不多。唯一不同的是, withContext() 函数需要传一个线程参数。

4.Job

Job 是协程的句柄,使用 launch 函数创建的协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口,这里列举 Job 几个比较有用的属性和函数:

kt 复制代码
public interface Job : CoroutineContext.Element {

    // job 处于 active 状态时为 true,表示协程启动后、执行结束前并且没有被取消的状态
    // 如果 job 没有被取消或者执行失败,job 在等待它的孩子执行结束也被认为是 active 状态
    public val isActive: Boolean
    
    // job 正常或者异常结束,均返回 true
    // job 被取消或者失败导致结束执行也认为是 complete 状态
    public val isCompleted: Boolean
      
    // job 因为任何原因被取消,或者主动调用了 cancel() 方法,
    // 或者执行失败了,或者它的孩子或父亲被取消了,均返回 true
    public val isCancelled: Boolean
    
    // 启动与这个 job 关联的协程(如果该协程没有启动的话)
    // 如果此调用的确启动了协程,返回 true
    // 如果协程此前已经处于 started 或者是 completed 状态,则返回 false 
    public fun start(): Boolean
    
    // 用于取消 job,可同时通过传入 CancellationException 来标明取消原因
    public fun cancel(cause: CancellationException? = null)
    
    // 用于阻塞协程直到此 job 执行结束
    public suspend fun join()

    // 用于注册 job 结束运行时(不管由于什么原因)触发执行的 handler
    // 如果调用该方法时,job 已经执行结束,handler 会被立刻触发
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

}

Job 有以下几种状态,每种状态对应的 isActive、isCompleted、isCancelled 的属性值如下:

State isActive isCompleted isCancelled
New (optional initial state) false false false
Active (default initial state) true false false
Completing (transient state) true false false
Cancelling (transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false
kt 复制代码
fun main() {
    //将协程设置为延迟启动
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        for (i in 0..100) {
            //每循环一次延迟一百毫秒
            delay(100)
        }
    }
    job.invokeOnCompletion {
        log("invokeOnCompletion:$it")  // job 执行结束时触发
    }
    log("1. job.isActive:${job.isActive}")
    log("1. job.isCancelled:${job.isCancelled}")
    log("1. job.isCompleted:${job.isCompleted}")

    job.start()

    log("2. job.isActive:${job.isActive}")
    log("2. job.isCancelled:${job.isCancelled}")
    log("2. job.isCompleted:${job.isCompleted}")

    //休眠四百毫秒后再主动取消协程
    Thread.sleep(400)
    job.cancel(CancellationException("test"))

    //休眠四百毫秒防止JVM过快停止导致 invokeOnCompletion 来不及回调
    Thread.sleep(400)

    log("3. job.isActive:${job.isActive}")
    log("3. job.isCancelled:${job.isCancelled}")
    log("3. job.isCompleted:${job.isCompleted}")
}

打印如下:

csharp 复制代码
    [main] 1. job.isActive:false
    [main] 1. job.isCancelled:false
    [main] 1. job.isCompleted:false
    [main] 2. job.isActive:true
    [main] 2. job.isCancelled:false
    [main] 2. job.isCompleted:false
    [DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test
    [main] 3. job.isActive:false
    [main] 3. job.isCancelled:true
    [main] 3. job.isCompleted:true

5.Deferred

async 函数的返回值是一个 Deferred 对象,Deferred 是一个接口,继承自 Job,所以 Job 包含的属性和方法 Deferred 都有,其主要是在 Job 的基础上扩展了 await()方法。其代码如下:

kt 复制代码
public interface Deferred<out T> : Job {

    // 等待 T 的返回,不会阻塞线程,在 deferred 计算完成时恢复,并返回结果值。
    // 在 deferred 被取消时会抛出相应的异常。
    public suspend fun await(): T

    public val onAwait: SelectClause1<T>

    public fun getCompleted(): T

    public fun getCompletionExceptionOrNull(): Throwable?
}

CoroutineContext

协程总是在某个环境中执行,该环境由 CoroutineContext 中的 Element 来决定:

kt 复制代码
public interface CoroutineContext {
    
    //CoroutineContext 中的一个 element
    public interface Element : CoroutineContext {

    }
}    

CoroutineContext 使用以下元素集定义协程的执行环境:

  • Job:控制协程的生命周期
  • CoroutineDispatcher:将任务指派给适当的线程
  • CoroutineName:协程的名称,可用于调试
  • CoroutineExceptionHandler:处理未捕获的异常

1.Job

前面的代码也可以看到 Job 继承自 CoroutineContext.Element 接口:

kt 复制代码
public interface Job : CoroutineContext.Element {
}

可以通过 Job 来控制协程的生命周期,代码如下:

kt 复制代码
val job = Job()

val scope = CoroutineScope(job)

fun main(): Unit = runBlocking {
    log("job is $job")
    val job = scope.launch {
        try {
            delay(3000)
        } catch (e: CancellationException) {
            log("job is cancelled")
            throw e
        }
        log("end")
    }
    delay(1000)
    log("scope job is ${scope.coroutineContext[Job]}")
//    scope.coroutineContext[Job]?.cancel()
//    scope.cancel()
    job.cancel()
}

打印如下:

csharp 复制代码
[main] job is JobImpl{Active}@4b4523f8
[main] scope job is JobImpl{Active}@4b4523f8
[DefaultDispatcher-worker-1] job is cancelled

这里 job 可以通过 scope.coroutineContext[Job] 直接获取到。

2.CoroutineDispatcher

CoroutineDispatcher 也继承自 CoroutineContext.Element 接口,CoroutineDispatcher(协程调度器)用于指定协程运行于哪个线程。CoroutineDispatcher 可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者对执行协程的线程不做限制。

所有的协程构造器(如 launch 和 async)都接受一个可选参数,即 CoroutineContext ,该参数可用于指定要创建的协程和其它上下文元素所要使用的 CoroutineDispatcher。

Kotlin 提供四种 Dispatcher 用于指定在哪一类线程中执行协程:

  • Dispatchers.Default,默认的 Dispatcher,使用一个共享的后台线程池,表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时可以使用 Dispatchers.Default。
  • Dispatchers.IO,表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如执行网络请求时,为了能够支持更高的并发数量,此时可以使用 Dispatchers.IO。适合用于执行磁盘或网络 I/O 的任务,例如:使用 Room 组件、读写磁盘文件,执行网络请求。
  • Dispatchers.Unconfined,对执行协程的线程不做限制,可以直接在当前调度器所在线程上执行。
  • Dispatchers.Main,使用此调度程序可用于在 Android 主线程上运行协程,只能用于与界面交互和执行快速工作,例如:更新 UI、调用 LiveData.setValue()。但是这个值只能在 Android 项目中使用,纯 Kotlin 程序使用会出现错误。

看下面的代码:

kt 复制代码
fun main() = runBlocking<Unit> {
    launch {
        log("main runBlocking")
    }
    launch(Dispatchers.Default) {
        log("Default")
        launch(Dispatchers.Unconfined) {
            log("Unconfined 1")
        }
    }
    launch(Dispatchers.IO) {
        log("IO")
        launch(Dispatchers.Unconfined) {
            log("Unconfined 2")
        }
    }
    launch(newSingleThreadContext("MyOwnThread")) {
        log("newSingleThreadContext")
        launch(Dispatchers.Unconfined) {
            log("Unconfined 4")
        }
    }
    launch(Dispatchers.Unconfined) {
        log("Unconfined 3")
    }
    GlobalScope.launch {
        log("GlobalScope")
    }
}

运行后打印如下:

css 复制代码
[DefaultDispatcher-worker-2] Default
[DefaultDispatcher-worker-2] Unconfined 1
[DefaultDispatcher-worker-1] IO
[DefaultDispatcher-worker-1] Unconfined 2
[main] Unconfined 3
[DefaultDispatcher-worker-1] GlobalScope
[MyOwnThread] newSingleThreadContext
[MyOwnThread] Unconfined 4
[main] main runBlocking

从打印结果可以看出:

  • launch 函数不使用 Dispatcher 时,它从调用 launch 的地方继承上下文环境 ,即和 runBlocking 保持一致,均在 main 线程执行;
  • IO 和 Default 均依靠后台线程池来执行;
  • Unconfined 则不限定具体的线程类型,当前调度器在哪个线程,就在该线程上进行执行,因此上述例子中每个 Unconfined 协程所在线程均不一样;
  • GlobalScope 启动协程时默认使用的调度器是 Dispatchers.Default,因此也是在后台线程池中执行;
  • newSingleThreadContext 用于为协程专门创建一个新的线程,专用线程是一种成本非常昂贵的资源,在实际开发时必需在不再需要时释放线程资源,或者存储在顶层变量中以便在整个应用程序中进行复用;

3.CoroutineName

CoroutineName 用于为协程指定一个名字,方便调试和定位问题。

kt 复制代码
fun main() = runBlocking<Unit>(CoroutineName("RunBlocking")) {
    log("start")
    launch(CoroutineName("MainCoroutine")) {
        launch(CoroutineName("Coroutine#A")) {
            delay(400)
            log("launch A")
        }
        launch(CoroutineName("Coroutine#B")) {
            delay(300)
            log("launch B")
        }
    }
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

4.CoroutineExceptionHandler

协程的异常处理放到最后分析。

5.组合上下文元素

有时我们需要为协程上下文定义多个元素,此时就可以用 + 运算符。例如,我们可以同时为协程指定 Dispatcher 和 CoroutineName:

kt 复制代码
fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default + CoroutineName("test")) {
        log("Hello World")
    }
}

而由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,从而组成新的 CoroutineContext。比如,(Dispatchers.Main, "name") + (Dispatchers.IO) 的运行结果是:(Dispatchers.IO, "name")

异常处理

在线程池的异常处理中,我们会使用这两种方法:

  • 添加 try catch 语句;
  • 给线程设置 UncaughtExceptionHandler;

因为协程底层也是使用的 Java 的线程模型,所以上述方法在协程的异常处理中同样有效。我们先添加 try catch 语句试试,代码如下:

kt 复制代码
fun main() {
    runBlocking {
        launch {
            try {
                throw RuntimeException("Exception1")
            }catch (e : RuntimeException){
                log(e.message)
            }

            kotlin.runCatching {
                throw RuntimeException("Exception2")
            }.onFailure {
                log(it.message)
            }
        }
        log("Hello, World!")
    }
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

运行后打印如下:

csharp 复制代码
[main] Hello, World!
[main] Exception1
[main] Exception2

可以看到程序没有报错,说明添加 try catch 语句有效,其中 runCatching 是 Kotlin 中对 try catch 语句一种封装。

协程中也有自己的 ExceptionHandler ------ CoroutineExceptionHandler,官方更推荐我们使用 CoroutineExceptionHandler 来处理协程中异常,如下所示:

kt 复制代码
fun main() {
    runBlocking {

        val handler = CoroutineExceptionHandler{ _, _ ->
            log("there is an exception")
        }

        val scope = CoroutineScope(SupervisorJob() + handler)

        scope.launch {
            throw NullPointerException();
        }

        delay(1000)
        log("Hello, World!")
    }
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")

打印如下:

csharp 复制代码
[DefaultDispatcher-worker-1] there is an exception
[main] Hello, World!

这里定义了一个 CoroutineExceptionHandler,并在初始化 CoroutineScope 时将其传入,这样这个协程作用域下的所有子协程发生异常时都将被这个 handler 拦截。

这里使用了 SupervisorJob() ,原因是协程的异常是会传递的,默认情况下,当一个子协程发生异常时,它会影响它的兄弟协程与它的父协程。而使用了 SupervisorJob() 则意味着,其子协程的异常都将由其自己处理,而不会向外扩散并影响它的兄弟协程与它的父协程。

还有一点需要注意的是, CoroutineExceptionHandler 只能用于初始化 CoroutineScope 或者其直接子协程(即 scope.launch ),否则就算创建子协程时携带了 CoroutineExceptionHandler,也不会生效。

相关推荐
bytebeats1 天前
Kotlin 注解全面指北
android·java·kotlin
jzlhll1231 天前
kotlin android Handler removeCallbacks runnable不生效的一种可能
android·开发语言·kotlin
&岁月不待人&1 天前
Kotlin 协程使用及其详解
开发语言·kotlin
苏柘_level61 天前
【Kotlin】 基础语法笔记
开发语言·笔记·kotlin
大福是小强1 天前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数
大耳猫1 天前
Android Studio 多工程公用module引用
android·java·kotlin·android studio
良技漫谈2 天前
Rust移动开发:Rust在Android端集成使用介绍
android·程序人生·rust·kotlin·学习方法
北欧人写代码2 天前
idea java 项目右键new file时 为什么是 kotlin class 不是普通class
java·kotlin·intellij-idea
zhangphil2 天前
Android LoaderManager AsyncTaskLoader,Kotlin(4)
android·kotlin
大福是小强2 天前
004-Kotlin界面开发快速入水之TicTacToe
开发语言·kotlin·界面开发·gui·教程·桌面应用·快速入水