Kotlin中的协程 - 基本使用

前言

Kotlin是一种在Java虚拟机上运行的静态类型编程语言,被称之为Android世界的Swift,在GoogleI/O2017中,Google宣布Kotlin成为Android官方开发语言

Kotlin协程介绍

Java中我们使用多线程Thread来进行并发,在Flutter框架的Dart语言中我们使用Future的基于事件驱动的异步模型以及Isolate这种隔离的轻量级线程去实现并发操作,在Kotlin开发的Android项目中得益于一些优秀的框架比如Rxjava等去进行一些并发的操作,这里主要学习下Kotlin中的协程使用,以及与Java中的线程,Dart中的Isolate进行一些比较

协程(coroutines)是一种并发设计模式,您可以在Android 平台上使用它来简化异步执行的代码。协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念

Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率

协程的特点

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作
  • 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发

协程中的元素

Coroutine scope 协程的作用域
Job 任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期
Coroutine context 协程上下文,协程上下文里是各种元素的集合
Coroutine dispatchers 协程调度,可以指定协程运行在 Android 的哪个线程里
suspend 挂起函数,挂起,就是一个稍后会被自动切回来的线程调度操作

在Android Gradle中引入Coroutines

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'

如何启动协程序

  • runBlocking
  • GlobalScope.launch
  • async,await

runBlocking
运行一个新的协程,会阻塞当前线程,直到代码块运行完毕,由于是代码阻塞的,不是用于协程,而是设计用于单元测试

 * @param context the context of the coroutine. The default value is an event loop on the current thread.
 * @param block the coroutine code.
 * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
 * This function should not be used from a coroutine. It is designed to bridge regular blocking code
 * to libraries that are written in suspending style, to be used in `main` functions and in tests.
public fun  runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
fun test() {
    Log.e("Mike", "Test Start")
    runBlocking {
        Log.e("Mike", "runBlocking start ${Thread.currentThread().name}")
        Thread.sleep(2000)
        Log.e("Mike", "runBlocking end ${Thread.currentThread().name}")
    }
    Log.e("Mike", "Test End")
}

运行结果
Test Start
runBlocking start main
//两秒后
runBlocking end main
Test End

GlobalScope.launch

会启动一个新的协程,而不会阻塞当前线程,返回一个协程JobJob可用来控制对应协程
序,比如cancel

参数一 context CoroutineContext
协程的上下文对象,不是AndroidContext,默认值为EmptyCoroutineContext

参数一start CoroutineStart
协程的启动配置,默认值为CoroutineStart.DEFAULT

参数一 block suspend CoroutineScope.() -> Unit
执行的代码块

返回值 Job
用于控制此携程的生命周期

 * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
 * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
 * @param block the coroutine code which will be invoked in the context of the provided scope.
 * Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
 * The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
fun test() {
    //三种方式使用携程
    //1.runBlocking 顶层函数
    Log.e("Mike", "Test Start")
    GlobalScope.launch {
        //this is CoroutineScope
        Log.e("Mike", "launch start ${Thread.currentThread().name}")
        Thread.sleep(2000)
        Log.e("Mike", "launch end ${Thread.currentThread().name}")
    }
    Log.e("Mike", "Test End")
}
运行结果
Test Start
Test End
launch start DefaultDispatcher-worker-1
//两秒后
launch end DefaultDispatcher-worker-1

async,await

之前在Dart语言中在基于事件的异步模型中,await用于等待耗时事件的返回,会阻塞函数后续执行,只能用在async函数中,async用于函数的后缀,表示此函数是一个异步函数,并有一个未来结果返回
Dart语言学习-异步编程async和await
Deferred表示延期返回,对应Dart语言中的Future也就是未来返回
Dart语言学习-异步编程Future

在Kotlin协程中,它们拥有类似的功能
async
CoroutineScope的函数,返回值Deferred,表示一个延期返回的结果

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

await
Deferred对象的函数,用于等待未来结果的返回,会阻塞当前线程直到返回结果

public suspend fun await(): T

所以我们的执行流程为
1.在CoroutineScope对象中通过async执行一个耗时操作,返回一个Deferred对象
2.使用await等待Deferred的返回结果

使用async,await实现这样的场景
在程序开始启动时打印启动时间,获取远端数据,初始化配置,远端数据完成之后在主线程将其渲染

    fun init() {
        printInitTime() //打印启动时间
        GlobalScope.launch { //获取远端数据需要耗时,创建一个协程运行在子线程,不会阻塞
            val deferred = async { //使用 async 执行一个耗时任务,返回一个deferred 
                Log.e("Mike", "getNetworkData start in ${Thread.currentThread().name}")
                Thread.sleep(2000)
                "response data"
            }
            val response = deferred.await() //等待deferred 的返回
            Log.e("Mike", "getNetworkData end in${Thread.currentThread().name}")
            GlobalScope.launch(Dispatchers.Main) { //启动一个协程,运行在主线程
                Log.e("Mike", "display $response in ${Thread.currentThread().name}");
            }
        }
        initConfig() //初始化配置

    }

    private fun printInitTime() {
        Log.e("Mike", "current time is ${System.currentTimeMillis()}")
    }

    private fun initConfig() {
        Log.e("Mike", "initConfig");
    }

打印结果
current time is 1624871044577
initConfig
getNetworkData start in DefaultDispatcher-worker-3
//两秒后
getNetworkData end inDefaultDispatcher-worker-3
display response data in main

Dispatchers

上面我们使用到了Dispatchers.Main使协程运行到了主线程,不同的Dispatchers定义了不同的协程调度模式

Dispatchers.Main
Android主线程,也可以使用MainScope().launch { }

Dispatchers.Unconfined
沿用了当前CoroutineScope的线程策略

Dispatchers.Default
默认值的CoroutineDispatcher,JVM共享线程池,保证最大程度的并行性,线程数量等于CPU的核心数,最少为2

Dispatchers.IO
IO线程池,它默认为64个线程的极限或核的数量的最大值

CoroutineStart

表示协程的执行模式

CoroutineStart.DEFAULT
默认的协程执行模式,根据上下文立即执行

CoroutineStart.LAZY
懒惰模式,在后续需要使用到的时候才会根据上下文执行

CoroutineStart.ATOMIC
和默认的协程执行模式相同,会根据上下文立即执行,但是在协程开始之前无法对其进行cancel

CoroutineStart.UNDISPATCHED

非阻塞式挂起

协程有个特点就是支持非阻塞式挂起,阻塞的意思就是会阻断当前线程后面的代码不会执行,那么挂起是什么意思

dealyThread.sleep()

dealy

在之前的多线程开发中,我们使用Thread.sleep()去进行线程的休眠延时,阻塞当前线程的执行,在协程中我们使用dealy去进行延时操作,sleep会进行阻塞调用休眠完成之后才会进行后续,dealy则不会阻塞会导致当前协程非阻塞挂起,非阻塞意为不会阻塞其他协程,挂起意为当前协程会挂起等待

下面使用dealy的案例中Coroutine 1中的delay不会阻塞Coroutine 2协程的执行,只是会挂起自己,在执行完毕之后会停止挂起打印launch1 end

fun testHang(){
    Log.e("Mike", "testHang start")
    GlobalScope.launch {
        //Coroutine 1
        GlobalScope.launch(Dispatchers.Main) {
            Log.e("Mike", "launch1 start")
            delay(2000)
            Log.e("Mike", "launch1 end")
        }
        Log.e("Mike", "testHang next")
        //Coroutine 2
        GlobalScope.launch(Dispatchers.Main) {
            Log.e("Mike", "launch2 start")
            delay(2000)
            Log.e("Mike", "launch2 end")
        }
    }
    Log.e("Mike", "testHang end")
}
//打印结果
testHang start
testHang end
testHang next
launch1 start
launch2 start
//两秒后
launch1 end
launch2 end

下面使用sleep的案例中,会阻塞后续的执行,Coroutine 1执行会阻塞两秒,然后才会执行Coroutine 2

fun testHang(){
    Log.e("Mike", "testHang start")
    GlobalScope.launch {
        //Coroutine 1
        GlobalScope.launch(Dispatchers.Main) {
            Log.e("Mike", "launch1 start")
            Thread.sleep(2000)
            Log.e("Mike", "launch1 end")
        }

        Log.e("Mike", "testHang next")
        //Coroutine 2
        GlobalScope.launch(Dispatchers.Main) {
            Log.e("Mike", "launch2 start")
            Thread.sleep(2000)
            Log.e("Mike", "launch2 end")
        }
    }
    Log.e("Mike", "testHang end")
}
打印结果
testHang start
testHang end
testHang next
launch1 start
//两秒后
launch1 end
launch2 start
//两秒后
launch2 end

结构化并发

CoroutineScope

当我们每创建一个协程,就会有一个CoroutineScope,它为协程的作用域可以直接使用this引用或者省略

public interface CoroutineScope

结构化并发

结构化并发是一种编程范式,一种编写易读易维护并发程序的结构化方法。 结构化并发思想与结构化编程类似,即代码中并发任务有明确的入口和出口。 与超出当前代码作用域的并发任务相比,理解结构化并发代码更容易

线程的并发是非结构化的并发,因为当你创建多个线程的时候,他们之间并没有结构化的层级关系,所有线程的上下文都是整个进程

Kotlin协程的并发是结构化,因为每个协程都有一个作用域CoroutineScope,Scope之间存在父子的结构化关系

后续

协程是一个庞大的系统,学习的过程也需要沉淀,下一步计划研究协程的下列问题

Job
suspend
协程的生命周期控制
协程与主线程的交互
协程与线程的选择
协程的应用
协程的单元测试
为什么会有协程

欢迎关注Mike的

Android知识整理

你可能感兴趣的:(Kotlin中的协程 - 基本使用)