Kotlin 协程基本概念

184 阅读11分钟

协程是一种轻量级的、非抢占式的多任务处理方式,是一种在单线程内执行多个任务的技术。与线程不同,线程需要操作系统管理,而协程由程序本身管理。

协程允许在单个线程内同时执行多个任务,并且每个任务可以暂时挂起,等待其他任务完成,从而避免了线程阻塞的问题。协程可以通过简单的方式实现多任务处理,并且可以更加高效和灵活地进行任务调度。

协程的基本概念包括:

  • 任务:每个协程都是一个任务,任务可以是并行执行的。
  • 切换:当一个任务需要等待其他任务完成时,它可以让出执行权,从而允许其他任务继续执行。
  • 调度:协程的调度是在用户态完成的,不需要操作系统的支持,并且可以更加灵活地进行任务调度。
  • 协作:协程之间可以协作执行,可以通过通道(channel)或者其他同步机制来实现任务之间的协作。

协程和线程区别

  • 资源占用:线程是操作系统级别的资源,需要占用大量的系统资源,而协程是用户级别的资源,占用的系统资源比线程少,更适合在大量并发的场景中使用。
  • 创建和销毁:创建协程的代价比创建线程低,销毁协程的代价也比销毁线程低,适合在大量创建和销毁的场景中使用。
  • 切换方式:协程是协作式的,由程序本身主动请求切换,更加灵活和高效;线程是抢占式的,需要操作系统进行线程切换,更适合在强制执行任务顺序的场景中使用。
  • 同步机制:协程之间可以使用更加简单的同步机制进行协调,如协程间的通信机制;线程之间需要使用更加复杂的同步机制进行协调,如锁、信号量等。

总体来说,协程比线程更适合用于实现并发、轻量级的任务处理,线程更适合用于强制执行任务顺序的场景。

suspend function:挂起函数

  • suspend 方法是指在协程中能够被挂起的方法,它的执行将会暂停直到其完成,但不会阻塞线程。

  • suspend 用于暂停执行当前协程,并保存所有局部变量。如需调用suspend函数,只能从其他suspend 函数进行调用,或通过使用协程构建器(例如 launch)来启动新的协程。

  • 常使用的一些 suspend function:

  • delay()

  • 用于在协程中延迟执行一段时间。它允许您在不阻塞线程的情况下暂停协程的执行。在延迟期间,其他协程可以继续执行。

  • yield()

  • 放弃当前协程的占用权,它可以暂停协程的执行,并将执行权交给其他协程。

  • suspendCancellableCoroutine()

  • 项目中常用的将回调改为协程的一个方法。

  • 在完成或者错误时, 调用 continuation.resume()

  • 注意:取消是协作的

  • 一个典型的suspendCancellableCoroutine最佳实践是在异步操作完成时回调协程,如网络请求。下面是一个使用suspendCancellableCoroutine的示例代码:

  •   suspend fun getResult(): String = suspendCancellableCoroutine { cont ->
          val callback = object : Callback {
              override fun onSuccess(result: String) {
                  cont.resume(result)
              }
      
              override fun onError(error: Throwable) {
                  cont.resumeWithException(error)
              }
          }
          makeNetworkRequest(callback)
      }
      
    

在上面的代码中,我们使用了suspendCancellableCoroutine函数,在makeNetworkRequest方法完成时,它将回调协程,并使用cont.resume(result)或cont.resumeWithException(error)在协程上恢复结果或异常。

CoroutineScope

表示协程运行的作用域,常用于与业务生命周期绑定。 业务可以在离开的时候对 CoroutineScope 进行 cancel。如:lifecycleScope

主要有以下分类:

1)顶级作用域:没有父协程的协程所在的作用域,也就是最顶层的作用域。在顶级作用域中,我们可以使用 GlobalScope 来创建协程,或者在 runBlocking 或者 coroutineScope 中创建协程。在顶级作用域中创建的协程是独立的,它们不受其他协程的影响,并且不受任何取消的影响。

2)协同作用域:指在已有的协程作用域中再创建一个协程作用域,并且它与原来的协程作用域共享相同的协程上下文(包括CoroutineDispatcher、Job等)。这样的目的是为了在当前协程作用域中启动一组相关的协程,并且可以一起结束。当前协程作用域中的协程可以使用coroutineScope函数来创建协同作用域,异常双向传播,异常会向上向下传播

3)主从作用域:指的是在一个协程中再创建另一个协程的形式,可通过supervisorScope创建。主协程的作用域成为主作用域,而从协程的作用域称为从作用域。当主作用域结束时,从作用域也会被取消,也就是说,从作用域是主作用域的一个子作用域。这种模式常用于组织一组相关的协程,以简化管理和取消这些协程的代码。异常自上而下传播,父协程不会受理子协程产生的异常,但是一旦父协程出现了异常,则会直接取消子协程。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // 协同作用域
    launch {
        delay(1000L)
        println("Task from runBlocking")
    }

    // 主从作用域
    coroutineScope {
        launch {
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope")
    }

    println("Coroutine scope is over")
}
//输出如下
Task from coroutine scope
Task from nested launch
Coroutine scope is over
Task from runBlocking

在上面的代码中,可以看到协同作用域是在runBlocking函数内启动的,而主从作用域是在coroutineScope内启动的。当coroutineScope完成时,主从作用域内的任务也会完成,但协同作用域不会受到影响。因此,主从作用域是在协同作用域内启动的一个子作用域,并且在主从作用域完成时,它的任务也会完成。

每一个 CoroutineBuilder 都需要 CoroutineScope, ScopeBuilder 有以下几种:

  • GlobalScope

  • 是 worker 线程

  • GlobalScope(object关键词修饰,其实就是个单例)不受job任何边界限制。

  • GlobalScope用于启动顶级协程,在整个应用程序生命周期内运行且不会过早取消。

  • 应用程序代码通常应使用应用程序定义的CoroutineScope。不建议GlobalScope在应用中使用。

  • MainScope

  • 实现方式:ContextScope(SupervisorJob() + Dispatchers.Main)

  • suspend coroutineScope

  • Creates a CoroutineScope and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides the context's Job.

  • 会打破 context 的继承关系。

CoroutineContext

协程的上下文, 主要由以下四个 element 组成:

Job

  • 协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled);
  • Job 是协程的句柄。使用 launchasync 创建的每个协程都会返回一个 Job 实例(async 返回的是 Deferred,直接继承自 Job ),该实例唯一标识协程并管理其生命周期。
  • Is part of its CoroutineContext:可以由 coroutineContext[Job] 来取出
  • 主要 api
    • 生命周期控制, start, cancel
    • 状态回调:join, invokeOnCompletion

CoroutineDispatcher

  • 指定协程运行的线程(IO、Default、Main、Unconfined);

  • Dispatchers.Main:主线程

  • Dispatchers.IO:专门优化适用于网络或磁盘IO

  • Dispatchers.Default:a shared background pool of threads,专门优化适用于 CPU 密集型作业

  • Dispatchers.Unconfined:

  • 其他的都是 confined

  • 哪个线程 invoke suspend 用哪个线程

  • 适用于:非 UI, 非 CPU 密集事情

  • newSingleThreadContext:

  • Creates a coroutine execution context using a single thread with built-in yield support.

  • 高成本的。需要考虑销毁或者复用问题

CoroutineName

 指定协程的名称,默认为coroutine

CoroutineExceptionHandler

指定协程的异常处理器,用来处理未捕获的异常。

其他的还包括:NonCancellable ...

  • NonCancellable 最好别用在 launch async 的参数中。

CoroutineBuilder

创建 Coroutine 的方法, 大都需要接收一个 CoroutineScope,可以自定义 CoroutineContext 去 override 父协程的一些设置。也可以定义一些 Start 来实现不同的效果

  • runBlocking

  • 用于在协程的上下文中同步地执行代码块。"runBlocking"函数阻塞当前线程,直到代码块执行完毕。这意味着,在使用"runBlocking"函数时,不需要等待其他协程完成。

  • CoroutineScope.launch

  • 最常用的构建器, 返回一个 Job,Job 中不携带任何自定义返回值,该对象可用于管理协程的生命周期,例如取消协程。

  • CoroutineScope.async

  • start a separate coroutine。

  • 返回一个 Deferred

    • 相对于 Job 的不同点是, 会返回一个结果
    • async 返回一个 Deferred - a light-weight non-blocking future
      • 可以使用 deferred.await() 来获取结果。
      • 实际上也是一个 Job
    • concurrency with coroutines is always explicit。
  • Async-style functions

    • 可以在非协程中定义 async, 只是需要制定一个 Scope, 并且在协程体中 await
  • CoroutineScope.future

  • 返回一个 Java 风格的 CompletableFuture

  • 不支持 CoroutineStart.LAZY

  • CoroutineScope.produce

  • For Channel

主要接收参数

  • CoroutineContext:override 父协程的一些 context, 常用于指定名字, 或者运行线程
  • CoroutineStart:不是 CoroutineContext,用于指定运行方式

DEFAULT

立即进入待调度状态

LAZY

只有需要(start/join/await)时才开始调度

ATOMIC

和DEFAULT类似,且在第一个挂起点前不能被取消

UNDISPATCHED

立即在当前线程执行协程体,直到遇到第一个挂起点(后面取决于调度器)

Cancelling

  • 取消是协作的,若需要响应 cancel,业务代码需要判断 isActive;

  • 在 kotlinx.coroutines 中的所有 suspend 都是 cancellable 的;

  • 父亲 cancel 了, 所有儿子 coroutine 都 cancel

    fun main() { runBlocking { launch { delay(500) log("launch finish") } async { delay(500) log("async finish") } coroutineScope { delay(500) log("coroutineScope finish") } MainScope().launch { delay(500) log("MainScope finish") } cancel() } } // 输出如下 [main] coroutineScope finish Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@1d119efb [AWT-EventQueue-0] MainScope finish

  • 会在"协作"的地方产生一个 CancellationException, 如果 catch 住了, launch 不会停止

  • 可以使用 withContext(NonCancellable){} 来使得块中的所有 suspend function 忽略已经取消的这个事情

    • 注意, 不要在 launch 或 async 的参数中用 NonCancellable
  • 经常在 finally 中, 做资源释放操作

    try { repeat(1000) { i -> println("job: I'm sleeping $i ...") delay(500L) } finally { withContext(NonCancellable) { println("job: I'm running finally") delay(1000L) println("job: And I've just delayed for 1 sec because I'm non-cancellable") } }

Timeout

  • 可以使用 withTimeout 来进行超时时间的设置
  • Timeout 可能在任何时候出现, 需要合理掌握释放资源时机。
  • 使用一个TimeoutCancellationException 来取消,是 CancellationException 的子类

Exceptions handling

Coroutine builder 分为两类,注意这两类的区分只是在 root 协程的声明时, 子协程不管是哪种声明都是向上传递 Exception:

fun main() = runBlocking {
    val job = GlobalScope.launch { // root coroutine with launch
        log("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    log("Joined failed job")
    val deferred = GlobalScope.async { // root coroutine with async
        log("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        log("Unreached")
    } catch (e: ArithmeticException) {
        log("Caught ArithmeticException")
    }
}
//输出如下
[DefaultDispatcher-worker-1] Throwing exception from launch
[main] Joined failed job
[DefaultDispatcher-worker-1] Throwing exception from async
[main] Caught ArithmeticException
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
  • propagating exceptions automatically: launch & actor(废弃了)

  • 会在 CoroutineExceptionHandler 中处理

  • exposing them to users: async & produce

    • 会在 await() 或 receive() 的时候暴露
    • 所有的 Exception 都放在 deferred 中
    • CourtineExceptionHandler 没有作用
    • 比较神奇的是,如果 try 一个子 async 的 await,也会 catch 住, 并且也会走 root 的 CoroutineExceptionHandler 和取消所有其他协程。

    fun main() { runBlocking { GlobalScope.launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("handler handle") }) { launch { log("start launch") delay(1000) log("launch finish") } val deferred = async { throw Exception("test") } kotlin.runCatching { deferred.await() }.getOrElse { log("catch exception") } }.join() } } //输出如下 [DefaultDispatcher-worker-2] start launch [DefaultDispatcher-worker-2] catch exception [DefaultDispatcher-worker-2] handler handle

  • 可以在 context 中传递一个 CoroutineExceptionHandler

  • 往往用于日志打印和异常处理, 不会无视这个 Exception 继续下面的逻辑

  • 子会直接给父传递 Exception,所以非根协程的 Handler 无效

    fun main() = runBlocking { GlobalScope.launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("parent handler catch exception") }) { val job = launch(CoroutineExceptionHandler { coroutineContext, throwable -> log("child handler catch exception") }) { throw Exception("child throw exception") } job.join() }.join() } //输出如下 [DefaultDispatcher-worker-1] parent handler catch exception

  • async 会将所有 Exception 放在 await 中, 所以给 async 设置无效

  • 遇到 Exception 时,整个流程是:

    • 结束出 Exception 的当前 Coroutine
    • cancel 根协程,进而 cancel 所有树中的协程 with that exception
    • Exception 给到 handler 中(Async 给到 await 中)
  • Exceptions aggregation

    • 在上述流程中如果仍出现 Exception,那么第一个出现的 Exception Wins。在异常产生后的流程中又产生的 Exception 被 attach 在第一个 Exception 后
  • 监督作业(Supervisor)

  • 只有取消向下传递,Exception 不向上传递,多个子 Coroutine 中一个发生异常, 不会取消其他的。

  • 每个 child 协程解决自己的 exception

  • supervisorScope 同理 

  • MainScope 就是其中一种 

Reference