«

Java崽学习Kotlin---协程快速入门

ZealSinger 发布于 阅读:60 Kotlin


协程

虽然说kotlin支持协程是它的一大特点,但是实际上Kotlin 仅在标准库中提供最基本底层 API 以便其他库能够利用协程。与许多其他具有类似功能的语言不同,asyncawait 在 Kotlin 中并不是关键字,甚至都不是标准库的一部分,其实也可以理解为kotlin只是在最底层提供了API能利用协程,但是本质上不是kotlin自己支持的

所以使用协程,首先就得需要导入对应的依赖,主流的就是kotlinx-coroutines-core,是Jetbains封装的协程库

---Maven----
<dependency>
    <groupId>org.jetbrains.kotlinx</groupId>
    <artifactId>kotlinx-coroutines-core</artifactId>
    <version>1.10.2</version>
</dependency>

---Gradle---
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}

基本概念

协程的实现方式有点类似于线程,下面是一个段协程的案例代码,在如下代码中,如果使用线程的方式,其实就是将GlobalScope.launch{....} 换成new Thread(......).start,并且将delay替换为Thread.sleep();两者的效果是一致的,下面代码能看得懂个大概就行,我们后面详细解释,大致体验一下kotlin中的协程

import kotlinx.coroutines.*
fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

创建作用域

按照上面基本概念中说的,每个协程首先必须得运行在一个协程作用域中,在Kotlin的协程库中,有很多的构造器方法可以用于创建协程作用域,但是基本上都不是直接创建的协程作用域对象的


GlobalScope

如上案例代码中,我们的协程代码就是在GlobalScope.launch{......} 结构中,所以很明显,GlobalScope也是一个可以创建作用域的函数,其会创建一个在整个Kotlin中都可以直接使用的协程作用域,显然,这个范围很大,不方便我们进行细致化控制和管理协程,所以实际上用的比较少

当我们使用的时候,Idea都会给我们黄色的警告

image-20250504211737616

需要注意的是,GlobalaScope是创建的作用域,而不是协程的创建,真正创建协程的函数是后面的 launch()以及另外一个 async()函数(即GlobalScope.async{......}也可以),具体区别和讲解在下面


coroutineScope函数(最常用)

最常用的创建协程作用域的方式是coroutineScope函数,其作用就是创建一个CoroutineScope,执行放在其内部的协程并且等待内部所有协程完毕之后再退出/返回

我们将上述案例进行改造,我们将打印World的逻辑提取出来作为一个函数,因为这个函数肯定是需要协程异步执行的,所以这里需要使用suspend修饰

然后这里也可以看到,线程中使用Thread.sleep进行阻塞休眠,协程中使用delay()进行阻塞休眠

fun main() {
    GlobalScope.launch {
        doWorld()
    }
    println("Hello,") // 先打印这个 协程此时还在delay(2000L)等待中
    Thread.sleep(3000L) // 主线程中主动休眠保证在协程完成之前不会结束进程,确保协程执行完毕
}

// suspend修饰
suspend fun doWorld(){
    coroutineScope {
        launch {
            delay(2000L)
            println("World!")
        }
    }
}

runBlocking函数

从上面代码中我们可以看到,主线程的Thread.sleep()好像是必须的,但是其实,我们还有一个runBlocking函数来创建作用域,其特点是:会阻塞线程,保证调用线程一定比其内部的协程晚点运行和退出,可以知道,这个作用一般作用域主线程中

fun main() {
    runBlocking{
        launch {
            doWorld()
        }
        println("Hello,") // 因为deworld还在delay 先打印这个
    } // 阻塞main线程的执行 保证进程不会结束
}

suspend fun doWorld(){
    coroutineScope {
        launch {
            delay(2000L)
            println("World!")
        }
    }
}

框架中的CoroutineScope

大多数情况下我们自己不需要创建协程作用域,而是框架会为我们准备好,就例如Jetpack中的ViewModel,其作用就是将UI操作的逻辑封装起来,那么所有的ViewModel中的协程操作都会运行在viewModeScope环境内

运行上下文

协程可以理解为是一个可以异步执行的函数,launch{....}创建协程并且执行{.....}中的协程逻辑,协程代码块执行完毕之后就会立马去执行{}外面的逻辑

协程代码块的执行是在写成里面的,什么时候返回是未知的,也可以让协程挂起,协程挂起之后就会释放运行他的线程,并不会阻塞运行他的线程,也就让别的协程有运行的机会

例如上述GlobalScope作用域中调用doWorld协程异步方法,doWorld()方法就是挂起状态,不会锁主main线程,这也是为啥 println("Hello")会被先执行和打印,main线程没有被阻塞而是会继续进行

协程我们说了可以理解为一个函数,那么函数肯定执行在一个线程环境中或者理解为被在某个地方被调用执行,这个协程运行的线程环境一般就是协程运行上下文,一般而言,除非指定切换线程执行,协程一般就是在主线程中被运行

协程的运行环境由CoroutineContext来定义,但是也同样,我们一般不会直接创建这个对象,而是通过参数或者其他构建函数来指定协程的运行上下文环境

通过参数指定上下文环境

创建协程的方法有launch和async,这两个方法可以有多个参数,其中都包含了指定运行上下文的相关参数,其方法的完整参数如下

fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

第一个参数就是指定运行上下文,那么可以写出如下案例代码,指定协程的运行上下文/线程环境

import kotlinx.coroutines.*
fun main() {
    runBlocking{
        launch {
            doWorld()
        }
        println("Hello,")
    }
}

suspend fun doWorld(){
    coroutineScope {
        // 指定协程环境
        /*
        Dispatchers是Kotlin中为了方便决定协程的运行上下文/线程环境定义的调度器类
        主要包括了Dispatchers.Default , Dispatchers.io
        Dispatchers.Main  Dispatchers.Unconfined四种已经定义好的方案,当然也可以自定义调度器
        四个默认调度器的底层其实就是Executors即线程池,不同的策略底层的线程池的相关参数配置也不同,适用于不同的应用场景

        Dispatchers.Default,底层是一个核心线程数等于CPU核心数的线程池,适合CPU密集行的任务
        Dispatchers.IO 底层是一个弹性线程池,可以按照需求进行拓展核心线程数,适合IO密集型任务
        Dispatchers.Main  即调用主线程作为线程环境,一般用于更新UI/接交互
        Dispatchers.Unconfied 不限制协程的线程环境,随着调用线程启动,岁恢复线程继续执行,容易导致协程在不同的线程中执行,慎用

        */

        launch(Dispatchers.Default){ 
            delay(2000L)
            println("World!")
        }
    }
}

拓展函数withContext

除了上述的在launth 和 async函数的参数中指定运行环境,还可以使用拓展函数withContext指定上下文环境,其接受两个参数,第一个参数也是CoroutineContext上下文环境,第二个参数就是协程函数的lambda表达式,该函数就会用第一个参数环境去执行第二个参数协程逻辑

其函数原型如下

suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T{....}

将上述案例修改,使用withContext指定运行上下文环境

fun main() {
    runBlocking{
        launch {
            doWorld()
        }
        println("Hello,")
    }
}

suspend fun doWorld(){
    coroutineScope {
        // 下面两种效果是一模一样的 只是两种写法
        // 按照函数原型标准调用方式
        launch{
            withContext(Dispatchers.Default,{
                delay(2000L)
                println("World!")
            })

            // 可以看之前的基础语法中的函数部分的入参相关设计中有说到,最后一个参数为lambda的时候在调用的时候可以将lambda逻辑写到()外边
            withContext(Dispatchers.Default){
                delay(2000L)
                println("World!")
            }
        }
    }
}

withContext通常用于一些需要对外不透明的运行上下文的设置,对外提供suspend方法允许被调用,但是suspend方法中自己有定义环境而不需要受限于或者说去关心调用者/用户的运行环境

如下 UserRepository对外提供login方法,它只向外部暴露一些suspend方法,在这些suspend方法内部通过withContext来指定它自己运行的上下文环境,从而不用管调用者的执行环境,不也需要调用者知道repo的执行环境

class UserRepository(
    val dispatcher: Dispatcher = Dispathers.IO
) {
    suspend fun login() {
        withContext(dispatcher) {
            // Do login
        }
    }
}

框架自带的运行上下文环境

虽然可以自己制定运行参数,如果全部自己定义也就意味着更多的管理和很多的context,事实上,现在很多并发框架都有对应的自己的配置好的运行上下文环境,不需要我们会过多考虑或者说不需要我们很细致的操作

协程可控性(Job对象和Deferred对象)

我们知道,Java中对于一个异步任务,我们想要知道其是否完成或者说获取其完成后的结果,是通过Futrue或者COmplateableFutrue实现的,可以通过Futrue判断线程是否完成和获取结果

对于协程,Kotlin存在类似的用于控制协程的机制,主要是依靠的Job对象和Deferred对象,launch创建的协程返回的是Job对象,async创建的协程返回的是Deferred对象,同时这也是之前没说的一个点,launch创建协程和async创建协程的区别

fun main() {
    runBlocking{
        launch {
            doWorld()
        }
    }
}

suspend fun doWorld(){
    coroutineScope {
        val job:Job = launch{
            delay(3000L)
            println("Coroutines#1")
        }

        val deferred = async{
            delay(1000L)
            println("Coroutines#2")
        }

        println("主线程同时正在执行别的任务.....")
        // 注意两者对应的方法名是不一样的  job是join()  deferred是await()
        job.join()
        deferred.await()
        println("All coroutines have finished.")
    }
}

/*  输出如下

主线程同时正在执行别的任务.....
Coroutines#2
Coroutines#1
All coroutines have finished.

*/

Job对象和Deferred对象之间除了和协程的创建方式不同之外,还有如下的区别

image-20250506172204487

Kotlin 编程