Skip to content

Kotlinの协程

更新: 12/7/2025 字数: 0 字 时长: 0 分钟

众所周知,在Kotlin协程发布的时候,Java的VirtualThread还不知道在哪呢,那么和Java互通的Kotlin是如何实现的协程呢?

基于Thread进行包装

这是目前主流的协程设计方案虽然实践不同,但Go语言实际上也是通过维护线程的方式实现的协程

而作为Google私生子的Kotlin在一定程度上借鉴了Go语言协程的优秀设计,同时有对老牌的Java多线程方案进行了参考谢谢你RxJava,也就形成了我们现在使用的Kotlin协程

协程的使用

kotlin
fun main(){  
  
    CoroutineScope(Dispatchers.IO).launch {  
        sleep(2000L)  
        println("协程内")  
    }  
  
    println("协程外")  
}

我们往往会使用CoroutineScope.launch{}的方式启动一个协程,协程的内容就是在代码块中执行的,其中协程和当前线程是并行的,也就是两者互不干预

CoroutineScope会要求我们传入一个参数,这个参数用于定义协程的上下文

这里的Dispatchers叫做协程调度器,用于定义协程是以何种方式运行(或者说是协程底层的线程池是如何实现的),常用的基本只有三个

  • Dispatchers.Default:用于CPU密集型操作
  • Dispatchers.IO:用于IO密集型操作
  • Dispatchers.Main:用于客户端程序,指明当前协程实在UI线程上运行,如果是服务端程序(比如SpringBoot程序),使用该调度器会报错

当然,Kotlin也为我们提供了自定义调度器的方法,但是不算常用,这里就不提了(希望在JDK25之后能够提供Dispatchers.VT版本的调度器)

父子协程

我们可以在协程的内部继续切换协程

kotlin
fun main(){  
  
    CoroutineScope(Dispatchers.IO).launch {  
        launch(Dispatchers.Default){  
            throw Exception("Error")  
        }  
        delay(3000L)  
        println("1")  
    }  
  
    sleep(10000L)  
}

其中内部的协程我们就称之为子协程,外部的协程我们就称之为父协程,子协程支持传入一个调度器用于切换当前协程所用的底部线程池实现(这样的设计方便了我们进行线程切换),当不传入调度器的时候则会使用与父协程一样的调度器(准确来说应该是使用父协程的上下文)

当子协程抛出异常时,父协程会受到牵连,一同中断

挂起

在Kotlin中我们会使用suspend关键字来将一个方法声明为挂起方法,这个关键字在编译上并没有什么实际的作用,只是提示开发者不要随意使用挂起方法

那么如何理解挂起呢?

我们知道协程采用的是一种协作式的多任务执行方式,允许当前方法在等待IO或其他操作的时候主动的让出CPU,等到自己等待的东西完成后再恢复到之前的状态。这个过程就被称之为挂起。

也就是说,当当前方法挂起之后,当前方法是未运行完毕的,是要等待自己等待的东西运行完成后再继续运行的。

在Kotlin中,我们可以使用withContext来将让当前的方法挂起,然后执行withContext中的任务,等待这个任务执行完毕后在继续执行

kotlin
suspend fun main(){  
    withContext(Dispatchers.IO){  
        delay(3000)  
        println("内部")  
    }  
    println("外部")  
}

需要注意的是,withContext必须要传入一个调度器,这个调度器用于指定内部任务要采取何种策略执行

运行结果

image.png

我们很明显的发现main方法主动等待withContext内部的任务执行完后才继续执行

挂起与协程

只有协程才会有挂起的说法,但需要注意的是,挂起与协程并非语言层面的东西,也就是所任何语言都可以实现协程与挂起

说回Kotlin,我们会使用挂起方法来将外部的协程挂起,然后去执行挂起方法内部的东西,等执行完毕后挂起的协程再恢复执行。

但是需要注意的是,被挂起的协程让出的资源并非一定是(可以是,可以不是)给到执行挂起方法的协程使用,二者只是两个并列的释放和使用的关系,然后在时间上存在一定的顺序性。

同样的,如果你只是单纯的使用协程,而不涉及挂起操作,那么在时间顺序上是和线程是没有区别的

协程的创建

在Kotlin中,如果我们想要创建一个协程并对其使用应该是这个样子

kotlin

    val coroutine = suspend {
        println("In Coroutine.")
        5
    }.createCoroutine(object : Continuation<Int> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<Int>){
            println("Result: $result")
        }

    })

    coroutine.resume(Unit)

该方法创建出一个协程的实例,然后我们可以使用其resume方法来使用他

kotlin
suspend {
        println("In Coroutine.")
        5
    }.startCoroutine(object : Continuation<Int> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<Int>){
            println("Result: $result")
        }

})

初次之外,我们还可以使用其自带的startCoroutine方法来启动协程

协程的Receiver

协程本身还可以携带一个Receiver,他的最大作用在于对协程的代码体提供增强的作用,这里我们来写一个lauchCoroutine方法来实现这个功能

kotlin
  
fun <R,T> launchCoroutine(receiver:R,block: suspend R.()-> T){  
    block.startCoroutine(receiver,object : Continuation<T> {  
        override fun resumeWith(result: Result<T>) {  
            println("Coroutine End $result")  
        }  
        override val context: CoroutineContext = EmptyCoroutineContext  
    })  
}  
  
class ProducerScope<T> {  
    suspend fun produce(value: T){   
          
    }  
}  
  
fun callLaunchCoroutine(){  
    launchCoroutine(ProducerScope<Int>()) {  
        println("In Coroutine.")  
        produce(1024)  
        delay(1000)  
        produce(2048)  
    }  
}

我们可以看到,在使用我们定义的launchCoroutine方法创建的协程中可以直接使用ProducerScope中的方法

作用域除了提供一定的函数支持以外,还可以用来增加限制,比如我们对Receiver类中添加一个RestrictsSuspension注解,那么在他的作用之下,我们就无法调用外部的挂起函数了

kotlin
@RestrictsSuspension  
class ProducerScope<T> {  
    suspend fun produce(value: T){  
  
    }  
}

fun callLaunchCoroutine(){  
    launchCoroutine(ProducerScope<Int>()) {  
        println("In Coroutine.")  
        produce(1024)  
        delay(1000)  //报错  
        produce(2048)  
    }  
}

协程上下文

协程中还存在上下文,上下文我们并不难理解是什么,Spring中有SpringApplicationContext,Android有Context,他们都是用来完成资源的获取与配置功能

在我们最早的代码中写了

kotlin
override val context: CoroutineContext = EmptyCoroutineContext

其重点EmptyCoroutineContext是Kotlin自己为我们提供的一种上下文,就是空上下文,我们也可以自己自己自定义上下文

kotlin
fun main() {  
    launchCoroutine(Unit){  
        println("${currentCoroutineContext()[CoroutineName.Key]}")  
    }  
  
}  
  
fun <R,T> launchCoroutine(receiver:R,block: suspend R.()-> T){  
    val myContext = EmptyCoroutineContext+ CoroutineName("MyCoroutine")  
  
    block.startCoroutine(receiver,object : Continuation<T> {  
        override fun resumeWith(result: Result<T>) {  
            println("Coroutine End $result")  
        }  
        override var context: CoroutineContext =myContext  
    })  
}  
  
class ProducerScope<T> {  
    suspend fun produce(value: T){  
  
    }  
}

协程的拦截器

Kotlin协程的基础库中除了上下文外还引入了拦截器(Interceptor),它允许我们在拦截协程异步回调的时候恢复调用

kotlin
fun main() {
    suspend {
        println("这是正在执行的内容")
    }.startCoroutine(object : Continuation<Unit> {
        override val context: CoroutineContext= LogInterceptor()

        override fun resumeWith(result: Result<Unit>) {

        }
    })
}

class LogInterceptor: ContinuationInterceptor {
    override fun <T> interceptContinuation(continuation: Continuation<T>)=LogContinuation(continuation)

    override val key= ContinuationInterceptor

}

class LogContinuation<T>(
    private val continuation: Continuation<T>
): Continuation<T> by continuation {
    override fun resumeWith(result: Result<T>) {
        println("before resumeWith: $result")
        continuation.resumeWith(result)
        println("after resumeWith.")
    }
}

我们不难看出,拦截器其实也是属于上下文的一种,我们通过将拦截器定义为协程上下文,进而实现了类似AOP的效果

Kotlin协程的类别

根据协程自己是否维护一个调用栈,我们可以将协程区分为有栈协程和无栈协程,有栈协程的好处在于其可以在任意的位置挂起与恢复,根据Kotlin写成的主要开发者之一的Roman Elizarov在Stack Overflow上的回答,Kotlin的协程更加接近与有栈协程

本站访客数 人次      本站总访问量