Kotlin协程简介(一)

本文介绍了Kotlin协程的基础概念,对比了使用回调、CompletableFuture、RxJava和协程进行异步编程的方法。重点讲解了Kotlin协程中的suspend函数、CoroutineScope、CoroutineContext、CoroutineDispatcher、Job和Deffered以及Coroutine builders的用法,阐述了它们在协程中的角色和重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录:

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。

我们先从一个例子说起,发送一个带有认证的 post 请求,需要以下三个步骤,首先客户端向服务其发送一个得到token的请求,然后构造一个 Post 请求,最后将 Post 请求发出去。这三个请求都是耗时操作,而且请求和请求之间有着依赖的关系。
fun requestToken(): Token {
    delay(500L) // 模拟请求过程
    return token 
}

fun createPost(token: Token, item: Item): Post {
    delay(500L) // 模拟构造过程
    return post  
}

fun processPost(post: Post) {
    delay(500L) // 模拟请求过程
}

操作2依赖于操作1,所以把操作2作为回调放在操作1的参数内,由操作1决定回调时机。

fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    requestTokenAsync { token ->
        createPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

这种多层嵌套的方式比较复杂,而且不方便处理异常情况。

Java 8 引入的 CompletableFuture 可以将多个任务串联起来,可以避免多层嵌套的问题。

可以简单看一下API,具体的使用方法参考文章:
CompletableFuture 使用详解

方法作用
runAsync创建一个异步操作,不支持返回值
supplyAsync创建一个异步操作,支持返回值
whenComplete计算结果完成的回调方法
exceptionally计算结果出现异常的回调方法
thenApply当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
handle与thenApply相似,handle还可以处理异常任务
thenAccept与thenApply相似,但是没有返回值
thenRun与thenAccept相似,但是得不到上面任务的处理结果
thenCombine合并任务,有返回值
thenAcceptBoth合并任务,无返回值
applyToEither两个任务用哪个结果
acceptEither谁返回的结果快使用那个结果
runAfterEither任何一个完成都会执行下一步操作
runAfterBoth都完成了才会执行下一步操作
thenCompose允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。

知道了API后就可以这么写

fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    requestTokenAsync()
            .thenCompose { token -> createPostAsync(token, item) }
            .thenAccept { post -> processPost(post) }
            .exceptionally { e ->
                e.printStackTrace()
                null
            }
}

RxJava的用法跟CompletableFuture链式调用比较类似,这也是比较简洁,比较多人使用的方式:

fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    Single.fromCallable { requestToken() }
            .map { token -> createPost(token, item) }
            .subscribe(
                    { post -> processPost(post) }, // onSuccess
                    { e -> e.printStackTrace() } // onError
            )
}
suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... }  // 挂起函数
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    GlobalScope.launch {
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要异常处理,直接加上 try/catch 语句即可
    }
}

协程可以让我们使用顺序的方式去写异步代码,而且并不会阻塞UI线程。

我们写的有两个方法是挂起的函数(suspend function)

suspend fun requestToken(): Token { ... }
suspend fun createPost(token: Token, item: Item): Post { ... } 

首先要知道的是,挂起函数挂起协程的时候,并不会阻塞线程。

然后一个 suspend function 只能在一个协程或一个 suspend function 中使用,但是suspend function和普通函数使用方法一样,有自己的参数,有自己的返回值,那么为什么要使用suspend funtion呢?

我们可以看到delay函数是一个挂起函数 , Thread.sleep()是一个阻塞函数,如果我们在一个A函数可能会挂起协程,比如调用delay()方法,因为 delay() 是suspend function ,只能在一个协程或一个suspend function中使用,所以A函数也必须是suspend function。所以使用suspend funtion的标准是该函数有无挂起操作。

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
    }
}

CoroutineScope为协程的作用域,可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。

创建子scope的方式有许多种, 常见的方式有:

方法一:使用lauch, async 等builder创建一个新的子协程。

我们来看一下CoroutineScop接口

// 每个Coroutine作用域都有一个Coroutine上下文
public interface CoroutineScope {
    // Scope 的 Context
    public val coroutineContext: CoroutineContext
}

所以 CoroutineScope 只是定义了一个新 Coroutine 的 coroutineContext,其实每个 coroutine builder(launch
,async) 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext 和取消操作。我们以launch方法为例:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
  • 第一个参数 context,默认 launch 所创建的 Coroutine 会自动继承当前 Coroutine 的 context,如果有额外的 conetxt 需要传递给所创建的 Coroutine 则可以通过第一个参数来设置。

  • 第二个参数 start 为 CoroutineStart 枚举类型,用来指定 Coroutine 启动的选项。有如下几个取值:
    - DEFAULT (默认值)立刻安排执行该Coroutine实例
    - LAZY 延迟执行,只有当用到的时候才执行
    - ATOMIC 类似 DEFAULT,区别是当Coroutine还没有开始执行的时候无法取消
    - UNDISPATCHED 如果使用 Dispatchers.Unconfined dispatcher,则立刻在当前线程执行直到遇到第一个suspension point。然后当 Coroutine 恢复的时候,在继续在 suspension的 context 中设置的 CoroutineDispatcher 中执行。

  • 第三个参数 block 为一个 suspending function,这个就是 Coroutine 中要执行的代码块,在实际使用过程中通常使用 lambda 表达式,也称之为 Coroutine 代码块。需要注意的是,这个 block 函数定义为 CoroutineScope 的扩展函数,所以在代码块中可以直接访问 CoroutineScope 对象(也就是 this 对象)

结论:launch方法实际上就是new了一个LazyStandaloneCoroutine协程(isLazy属性为false),协程自动的继承了当前 Scope(this代表的协程scope) 的 coroutineContext 和取消操作。

方法二:使用coroutineScope Api创建新scope:

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

这个api主要用于方便地创建一个子域(相当于创建一个局部作用域),并且管理域中的所有子协程。注意这个方法只有在所有 block中创建的子协程全部执行完毕后,才会退出。

// print输出的结果顺序将会是 1, 2, 3, 4
coroutineScope {
          delay(1000)
          println("1")
          launch { 
              delay(6000) 
              println("3")
          }
          println("2")
          return@coroutineScope
      }
      println("4")

方法三:继承CoroutineScope.这也是比较推荐的做法,用于处理具有生命周期的对象。

在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的 Coroutine 只在该界面有意义,如果用户在等待 Coroutine 执行的时候退出了这个界面,则再继续执行这个 Coroutine 可能是没必要的。那么我们怎么让activity管理好其内的 Coroutine 呢?

我们来看下面的例子:

class ScopedActivity : Activity(), CoroutineScope {
    lateinit var job: Job
    // CoroutineScope 的实现
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 当 Activity 销毁的时候取消该 Scope 管理的 job。
        // 这样在该 Scope 内创建的子 Coroutine 都会被自动的取消。
        job.cancel()
    }

    /*
     * 注意 coroutine builder 的 scope, 如果 activity 被销毁了或者该函数内创建的 Coroutine
     * 抛出异常了,则所有子 Coroutines 都会被自动取消。不需要手工去取消。
     */
    fun loadDataFromUI() = launch { // <- 自动继承当前 activity 的 scope context,所以在 UI 线程执行
        val ioData = async(Dispatchers.IO) { // <- launch scope 的扩展函数,指定了 IO dispatcher,所以在 IO 线程运行
            // 在这里执行阻塞的 I/O 耗时操作
        }
        // 和上面的并非 I/O 同时执行的其他操作
        val data = ioData.await() // 等待阻塞 I/O 操作的返回结果
        draw(data) // 在 UI 线程显示执行的结果
    }
}

解释一下这个地方:get() = Dispatchers.Main + job

一个上下文(context)可以是多个上下文的组合。组合的上下文需要是不同的类型。所以,你需要做两件事情:

  • 一个 dispatcher: 用于指定协程默认使用的 dispatcher;
  • 一个 job: 用于在任何需要的时候取消协程;

操作符号 + 用于组合上下文。如果两种不同类型的上下文相组合,会生成一个组合的上下文(CombinedContext),这个新的上下文会同时拥有被组合上下文的特性。因为:get() = Dispatchers.Main + job,所以launch方法实际上是在Dispatchers.Main,也就是在UI线程中执行的。

CoroutineScope 可以理解为一个协程,里面有一个协程的上下文:CoroutineContext,这个协程上下文包含很多该协程的信息,比如:Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中,是用map来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context的信息:

val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]

CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有四种标准实现Dispatchers.Default、Dispatchers. IO,Dispatchers.Main 和 Dispatchers.Unconfined,Unconfined 就是不指定线程。

  • Dispatchers.Default: 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。
  • Dispatchers. IO: 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism 可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。
  • Dispatchers.Unconfined: 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point。也就是说,coroutine builder 函数在遇到第一个 suspension point 的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function 所在的线程。 一般而言我们不使用 Unconfined。
  • Dispatchers.Main: 是在 Android 的 UI 线程执行。
  • 通过 newSingleThreadContext 和 newFixedThreadPoolContext 函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。

CoroutineScope.launch 函数返回一个 Job 对象,该对象代表了这个刚刚创建的 Coroutine实例,job 对象有不同的状态(刚创建的状态、活跃的状态、执行完毕的状态、取消状态等),通过这个 job 对象可以控制这个 Coroutine 实例,比如调用 cancel 函数可以取消执行。Job对象持有所有的子job实例,可以取消所有子job的运行。Job的join方法会等待自己以及所有子job的执行, 所以Job给予了CoroutineScope一个管理自己所有子协程的能力。

CoroutineScope.async 函数也是三个参数,参数类型和 launch 一样,唯一的区别是第三个block参数会返回一个值,而 async 函数的返回值为 Deferred 类型。可以通过 Deferred 对象获取异步代码块(block)返回的值。Deferred 继承了 Job,它有个 await() 方法。

// Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
// returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
public suspend fun await(): T
  1. CoroutineScope.launch : 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,无返回值
  2. CoroutineScope.async : 在后台创建一个新协程,有返回值
  3. runBlocking :

创建一个新的协程来阻塞当前线程,直到 runBlocking 代码块执行完成。通常它不会用于协程中,因为在协程中写一个阻塞的代码块实在太别扭,可以通过挂起操作取代。它通常作为一个适配器,将 main 线程转换成一个 main 协程,我们也就持有了一个 main 协程的 coroutineContext 上下文对象,就可以随心所欲用(this)使用 coroutineContext 的扩展方法,随心所欲使用 suspend 方法 ( suspend 方法只能用于 suspend 方法和协程中)。所以 runBlocking 一般用在 test 函数和 main 函数中。

  1. withContext :

withContext 不会创建一个新的协程,在指定的协程上运行代码块,并挂起该协程直到代码块运行完成。通常是用于切换协程的上下文。

例如:

// 使用 withContext 切换协程,上面的例子就是先在 IO 线程里执行,然后切换到主线程。
GlobalScope.launch(Dispatchers.IO) {
    ...
    withContext(Dispatchers.Main) {
        ...
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值