基础概念

  1. 协程:是可挂起计算的实例。它在概念上类似于线程,因为它需要一个代码块来运行,该代码块与其余代码同时工作。但是,协程不绑定到任何特定线程。它可能会在一个线程中暂停执行,并在另一个线程中恢复执行。
  2. 结构化并发:协程的生命周期必须与作用域绑定,形成父子关系树。
  3. 挂起:挂起是协程在执行过程中​主动让出线程​的能力,挂起函数用suspend修饰。

协程上下文CoroutineContext

val defaultScope =
    CoroutineScope(SupervisorJob() + CoroutineName("hello Default") + Dispatchers.Default + CoroutineExceptionHandler { context, e ->
    })
  1. Job, 控制协程的生命周期,一般使用Job()或者SupervisorJob()。

    • Job():子协程失败会取消父协程和兄弟协程
    • SupervisorJob():子协程失败不影响其他协程
  2. CoroutineName,给协程上下文命名,默认是NULL,调试方便。

  3. Dispatchers,将协程调度到不同类型的线程。

    • Dispatchers.Default: cpu密集型计算场景,使用共享线程池(固定大小,线程数=CPU核心数)
    • Dispatchers.IO: 耗时操作场景,使用动态扩展线程池(优先使用共享线程池)
    • Dispatchers.Main: 主线程
    • Dispatchers.Unconfined: 不推荐
  4. CoroutineExceptionHandler,协程上下文异常处理

val thread: Thread get() = Thread.currentThread()
val ioScope =
    CoroutineScope(SupervisorJob() + CoroutineName("hello IO") + Dispatchers.IO)

fun main(): Unit = runBlocking {

    launch {
        println("hello launch_1: $thread + ${coroutineContext[CoroutineName]} + ${coroutineContext[Job]}")
    }

    launch(ioScope.coroutineContext) {
        println("hello launch_2: $thread + ${coroutineContext[CoroutineName]} + ${coroutineContext[Job]}")
    }
}

//输出:
hello launch_2: Thread[#30,DefaultDispatcher-worker-1,5,main] + CoroutineName(hello IO) + StandaloneCoroutine{Active}@4e9f373
hello launch_1: Thread[#1,main,5,main] + null + StandaloneCoroutine{Active}@305fd85d

启动协程

runBlocking

连接了常规fun main()的非协程世界和runBlocking { ... }大括号内的协程代码, 会阻塞当前线程直到所有内容执行完成。

仅在测试中使用,生产代码用不到。

launch

public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
当前协程的扩展方法,在当前协程下启动一个新的协程,返回的是Job对象,出现异常立刻传播,适用于不需要返回值的场景。

    val job = launch {
        println("launch_1: ${coroutineContext[Job]}")
    }
    println("job: $job")

//输出:
job: StandaloneCoroutine{Active}@1f7030a6
launch_1: StandaloneCoroutine{Active}@1f7030a6
async

public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T>
当前协程的扩展方法,在当前协程下启动一个新的协程,返回Deferred<T>, 使用deferred.await()获取结果,异常在await时才会传播。

    val deferred = async {
        delay(2000)
        "hello, sync"
    }
    deferred.await()
coroutineScope和supervisorScope

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R

挂起函数,上下文只能继承于父协程,主要作用是控制包含的子协程。

  • coroutineScope:子协程失败则所有取消
  • supervisorScope:子协程失败不影响其他协程
    coroutineScope {
        println("coroutineScope: start")
        launch {
            delay(2000)
            println("normal...")
        }
        launch {
            throw Exception("error...")
        }
        println("coroutineScope: end")
    }

//输出:
使用coroutineScope时,println("normal...")并不会执行,因为第二个子协程抛了异常。
将coroutineScope替换为supervisorScope,异常仍然抛,但是println("normal...")仍然会执行。
withContext

public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T
挂起函数,相比coroutineScope和supervisorScope多了协程上下文参数,一般传入协程调度器切换协程所在的线程

    println("global: ${coroutineContext[Job]}")
    withContext(Dispatchers.IO) {
        println("withContext: ${coroutineContext[Job]?.parent}")
    }

//输出:相同的job对象
global: BlockingCoroutine{Active}@51cdd8a
withContext: BlockingCoroutine{Active}@51cdd8a
GlobalScope

生命周期绑定应用进程的作用域,一般不使用它来开辟协程。

协程启动模式

launch和sync的声明中,除了协程上下文参数,还有一个CoroutineStart,代表协程的启动模式

CoroutineStart.DEFAULT

立即调度,可以立刻取消。

CoroutineStart.LAZY

不会立即调度,需要显示的使用Job的start(),join(),await()才会启动,可以立刻取消。

CoroutineStart.ATOMIC

立即调度,但是不能立刻取消,需要执行到第一个挂起点才行。

    val job = launch(start = CoroutineStart.ATOMIC) {
        println("launch----1---")
        delay(1000)
        println("launch--2-----")
    }
    job.cancelAndJoin()

//输出,会打印"launch----1---",换成DEFAULT的话就不会打印。
CoroutineStart.UNDISPATCHED

立即在当前线程执行,直到第一个挂起点后,在根据调度器去切换线程或者在其他上下文执行。

   launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.IO) {
        println("launch_1 thread: $thread")
        delay(1000)
        println("launch_2 thread: $thread")
   }
   println("main....")
//输出:第一个日志打印会现在主线程执行,到达第一个挂起点delay时再根据传入的调度器切换到线程池。
launch_1 thread: Thread[#1,main,5,main]
main....
launch_2 thread: Thread[#31,DefaultDispatcher-worker-1,5,main]

协程的生命周期

一定要阅读Job接口的类注解
通过打印协程上下文可以看到当前协程处于的状态,比如job=StandaloneCoroutine{Cancelling}@3e57cd70

                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+
初始状态

协程已构建,但是还没有调度执行,通过CoroutineStart.LAZY启动协程到这一阶段。
isActive = false, isCompleted = false, isCancelled = false

Active状态

通过job.isActive可以检查当前是否处于激活状态,挂起时仍然处于激活状态。
isActive = true, isCompleted = false, isCancelled = false

手动激活协程的方式:

  1. job.start(),启动job的相关的协程,如果启动成功返回true,如果协程已经启动或者已完成则返回false。
  2. job.join(),join是一个挂起函数,会阻塞当前的协程,直到job相关的协程执行完,调用join的job如果处于未激活状态,则会激活。
  3. deferred.await(),和join类似,会阻塞当前协程,区别是await会得到async协程的返回值。
Completed状态

协程执行完成或者return后会走到Completed状态,通过job.isCompleted查看。
isActive = false, isCompleted = true, isCancelled = false

当协程块中抛出异常结束协程,协程isCompleted也为true。

Cancelled状态

通过job.isCancelled检查协程是否触发了取消。
isActive = false, isCompleted = true, isCancelled = true

  1. 调用job.cancel()或者job.cancelAndJoin()取消协程
  2. 协程内抛出了异常。

协程的取消不是立即执行的,需要遇到挂起点或者手动调用状态函数,比如isActive才会触发取消。

Completing

完成前的过渡状态。
isActive = true, isCompleted = false, isCancelled = false

Cancelling

取消前的过渡状态。
isActive = false, isCompleted = false, isCancelled = true

协程的异常处理

协程的异常遵循结构化并发,子协程的异常会传播到父协程和兄弟协程,SupervisorJob协程内则不会传播。

异常处理方式
  1. 将协程块中的代码try-catch起来。
  2. 在当前协程上下文中的CoroutineExceptionHandler捕获。
CancellationException

协程取消时抛出的特殊异常,静默处理。

  1. 当需要在取消时做逻辑操作时,catch捕获CancellationException后需要继续抛出。
  2. 正常清理逻辑不需要捕获CancellationException,只需要在finally清理即可。

在finally中想要执行挂起函数,需要包含在withContext(NonCancellable)。

  1. 一般使用中,不需要手动抛出CancellationException。
TimeoutCancellationException

超时异常是CancellationException的子类,超时后协程自动取消,并抛出异常。

try {
    val result = withTimeout(1000) { // 1秒超时
        fetchDataFromNetwork() // 挂起函数
    }
    println("结果: $result")
} catch (e: TimeoutCancellationException) {
    println("操作超时")
    throw e
}
  1. withTimeout:挂起函数,指定超时时间,代码块中的在指定时间内没有执行完成就抛出异常并取消,返回代码块最后一行。
  2. withTimeoutOrNull:和withTimeout的区别时当超时取消时会返回null。

超时逻辑中,防止内存泄露,不要在超时函数后紧跟资源释放逻辑,应该放在finally中。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐