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 <T> 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 <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

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知识整理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容

  • 一些问题 程序什么时候需要切线程? 工作比较耗时:放在后台 工作特殊:需要放在指定线程(ui刷新、计算、io) k...
    A_si阅读 3,140评论 0 4
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    业志陈阅读 1,073评论 0 5
  • [TOC] 简介 Coroutines are computer program components that ...
    Whyn阅读 5,944评论 5 15
  • 在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种...
    Android开发指南阅读 839评论 0 2
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 125,311评论 2 7