首页 > 其他分享 >kotlin协程:一文搞懂各种概念

kotlin协程:一文搞懂各种概念

时间:2023-11-16 18:32:20浏览次数:22  
标签:协程 launch kotlin delay 线程 CoroutineScope 搞懂 Dispatchers

前言

使用 kotlin 协程已经几年了,可以说它极大地简化了多线程问题的复杂度,非常值得学习和掌握。此文介绍并梳理协程的相关概念:suspend、non-blocking、Scope、Job、CoroutineContext、Dispatchers 和结构化并发。

进入协程世界

简而言之,协程是可以在其内部进行挂起操作的实例,是否支持挂起函数也是协程世界和非协程世界的最大区别。初学者可以把协程看作是“轻量级线程”以做对比,但实际上他依然是跑在线程上的,所以也可以将它看作是一个强大的异步框架。

要使用协程,需要添加 kotlinx-coroutines-core 库的依赖。

挂起函数与非阻塞

先看一段代码:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    println("Hello World!")
}

runBlocking 是一个协程构造器,他连接了非协程和协程世界,{ } 里便是协程世界。这两个世界的差异在于是否可支持挂起操作:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
     delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
    println("Hello World!")
}

"Hello World!" 将会在进入协程 1s 后打印,delay 便是一个挂起函数,类似线程中 sleep 的作用。区别是挂起函数 delay 并不会阻塞当前线程。这里有两个概念,挂起和非阻塞,挂起的是协程,非阻塞的是线程。

从线程的角度看,假如进入协程时运行在线程 A,那么执行 delay 函数时运行在线程 B,执行完 delay 之后又在线程 C 上执行 println,线程 C 可能就是线程 A,也可能不是,整个过程可以简化为:在某个线程执行协程,在遇到挂起函数 delay 时切换到另一个线程,执行完 delay 后又切回某个线程继续执行协程。不阻塞线程即不阻塞挂起前协程所在的线程,即 A 线程,A 线程可以继续执行其他任务。那这有什么意义呢?试想协程一开始运行在 Android 中的 ui 线程,在挂起函数里执行耗时的网络请求,网络请求结束自动回到协程,继续在 ui 线程上的执行。一方面耗时任务未阻塞 ui 线程,另一方面完全消除了异步回调,这使异步任务变得极为简单:以同步的方式书写异步的代码。

从协程的角度看,协程遇到挂起函数时会被挂起,即暂停了,等待挂起函数执行完成,相比于线程阻塞,协程挂起几乎没有任何资源消耗,其本质上是回调。

另外,挂起函数都有 suspend 关键字修饰,编译器也会在此施加魔法:

public suspend fun delay(timeMillis: Long) { ... }

runBlocking 会阻塞当前线程直到协程执行完毕,因此适合单元测试,下面会介绍更合适的进入协程世界的方式。

注:例子中的线程 A、B、C 是通过协程中的 Dispatcher 控制的,后面聊到。

CoroutineScope

协程都是由 CoroutineScope 创建的,协程在创建时,都会关联到一个新的的 CoroutineScope

CoroutineScope 即协程作用域,它限制和控制协程的作用范围或者说生命周期。不仅当前协程会受其影响,所有在协程作用域内创建的子协程也会有关联。当调用 CoroutineScope 的 cancel 方法时,会取消当前协程以及其关联的所有下层协程。

自定义 CoroutineScope

进入协程世界除了上面使用的 runBlocking 方式。还可以自定义 CoroutineScope

fun main() {
    CoroutineScope(Dispatchers.IO).launch {
            ...
    }
}

上面代码创建了一个运行在 IO 线程环境的协程作用域并创建了一个协程,该协程运行在 IO 线程。

GlobalScope

此外,还可以使用 GlobalScope 这个全局的协程作用域进入协程世界:

fun main() {
    GlobalScope.launch(Dispatchers.IO) {
            ...
    }
}

GlobalScope 虽拿来即用,但它是全局的,生命周期太长,使用不当会导致内存泄露风险。

Android 中的 Scope

android 中,推荐使用 LifecycleOwner.lifecycleScope,他和 LifecycleOwner 的生命周期绑定,不会出现内存泄露的问题。

如果使用了 ViewModel,还可以使用 ViewModel.viewModelScope,同样和 ViewModel 生命周期绑定。

它们都在 UI 线程执行。

还有一个 MainScope 也在 UI 线程,有了上面两个,这个基本用不到了,因为他没绑定有生命周期的对象,需要手动 cancel。

Job

通过 CoroutineScope 创建的协程即 Job,可以认为 就是协程的实例。一个 Job 可以有多个子 ,也即一个协程可以有多个子协程。具有父子关系的协程,父协程取消时,所有子协程都会取消, 的 cancel 本质就是通过取消 实现的,因此 的 cancel 等价于 的 cancel。

val job = launch { // 1
    launch { // 2
        ...
    }
    
    launch { // 3
        ...
    }
}
job.cancel()

上面代码中 job 取消时会把 2、3处协程也取消。

协程层次化的好处是便于管理,再多的协程,只要它们具有相同的父协程,就可以方便地控制其生命周期。在层次化的协程结构中,取消事件自上而下,异常事件自下而上,这背后是结构化并发的思想。

特别的,SupervisorJob 是一种特殊的 Job,唯一的区别在于异常传播到 SupervisorJob 层会停止向上传播,将异常交由 SupervisorJob 处理,借助这一特点我们可以把异常传播控制在一定范围内。异常传播与处理的详细介绍之后会单独写~~

CoroutineContext

CoroutineContext ,协程上下文,是 CoroutineScope 的唯一成员,是用于存放协程执行环境的地方,如调度器(Dispatcher)、异常处理器(CoroutineExceptionHandler)、Job 等。CoroutineContext 的主要目的是提供一个统一的方式来管理协程的执行环境和属性。

CoroutineScope 在创建协程时会把 CoroutineContext 传递下去,新创建的协程会继承父协程或Scope 的 CoroutineContext

CoroutineContext 数据的使用类似 Map,根据 Key 取值,如果子协程创建时指定了 CoroutineContext ,则会合并,相同 Key 的值会被覆盖。

fun main() {
    CoroutineScope(Dispatchers.Main).launch(CoroutineName("My Coroutine")) {
        println("My Coroutine name: ${coroutineContext[CoroutineName]}")
    }
}

上面代码创建了一个在主线程的协程作用域,并创建了一个协程,该协程指定了协程元素-CoroutineName,这将和 CoroutineScope 中的 Dispatchers.Main 合并成新的 CoroutineContext

Dispatchers 与线程

Dispatchers 可以指定协程的执行的线程环境,不过它强调的是线程的类别而不是哪一个具体的线程。如:Dispatchers.IO 表示 IO 密集型线程池,Dispatchers.Default 表示 cpu 密集型线程池,特别的是,Dispatchers.Main 特指 Android 中的主线程。在协程中可以使用 withContext 进行线程池的切换:

fun main() {
    CoroutineScope(Dispatchers.Main).launch {
        withContext(Dispatchers.IO) {
            ...
        }
    }
}

如果是网络数据传输等 io 任务,一定要使用 Dispatchers.IO,其余计算类耗时任务使用 Dispatchers.Default,这是因为 Dispatchers.Default 线程池的线程数较少(和 cpu 核心数有关),而 Dispatchers.IO 线程池的线程数更多且可动态调整。io 任务往往等待时间更长,使用 Dispatchers.Default 的话很容易占满所有线程资源。

协程世界

如果已经在协程世界中了,那么创建新的协程的方式就比较多了,launch、async、coroutineScope、supervisorScope 等都可以方便地创建不同需求的协程。

launch、async

两者都是协程构建器,最大的区别是 async 有返回值而 launch 没有。有人说并行就用 async,其实它们都能并行,只不过业务场景里往往都需要拿到返回值。

完整测试用例:

fun main() = runBlocking {
    CoroutineScope(Dispatchers.IO).launch(CoroutineName("My Coroutine")) {
        println("My Coroutine name: ${coroutineContext[CoroutineName]}")

        coroutineScope {
            launch {
                println("task 1")
            }
            launch {
                println("task 2")
            }
        }

        coroutineScope {
            val task1 = async {
                delay(1000)
                1
            }
            val task2 = async {
                delay(1000)
                2
            }
            println(task1.await() + task2.await())
        }
    }.join()
}

coroutineScope、supervisorScope

coroutineScopesupervisorScope 也都协程构建器,只不过它们会等待协程执行结束才结束,有点像 runBlocking,但不同的是 runBlocking 阻塞当前线程,而它们只是挂起协程,一个是普通方法,另外两个则是挂起函数。

它们的差别类似 JobSupervisorJob 的差别,supervisorScope 中顶级子协程的发生异常不会影响其他顶级子协程。

supervisorScope {
    launch {
        throw Exception("error")
        delay(1000)
        println("task 1")
    }
    launch {
        delay(2000)
        println("task 2")
    }
}

上面代码 task 2 可以被正常打印出来,即使兄弟协程发生了异常。

结构化并发

它是一种编程范式,旨在通过结构化的方式使并发编程 更清晰明确、更高质量、更易维护。

其核心有几点:

  • 通过把多线程任务进行结构化的包装,使其具有明确的开始和结束点,并确保其孵化出的所有任务在退出前全部完成。
  • 这种包装允许结构中线程发生的异常能够传播至结构顶端的作用域,并且能够被该语言原生异常机制捕获。

kotlin 协程设计中的协程关系、执行顺序、异常传播/处理 都符合结构化并发。结构化并发明确了并发任务什么时候开始,什么时候结束,异常如何传播,通过控制顶层结构具柄就可实现整个并发结构的取消、异常处理,使复杂的并发问题简单、清晰、可控。可以说,结构化并发大大降低了并发编程的难度。

标签:协程,launch,kotlin,delay,线程,CoroutineScope,搞懂,Dispatchers
From: https://blog.51cto.com/u_16175630/8430703

相关文章

  • Kotlin Notes - 2
    PropertiesinKotlinclassescanbedeclaredeitherasmutable,usingthevarkeyword,orasread-only,usingthevalkeyword.//fullsyntaxfordeclaringapropertyvar<propertyName>[:<PropertyType>][=<property_initializer>]......
  • kotlin 泛型基础
    一、泛型函数如下是泛型函数的一种构造在fun函数标记的右边增加该函数要使用的类型形参fun<T>List<T>.slice(indices:IntArray):List<T>{valret=mutableListOf<T>()for(vinindices){ret.add(this[v])}returnret}listOf(3,4,5,6......
  • Kotlin委托的深入解析与实践
    引言在Kotlin编程语言中,委托是一项强大的特性,它能够极大地简化代码,提高代码的可维护性。本文将深入探讨Kotlin中的委托机制,介绍其原理、具体使用方式以及实际应用场景。委托的原理委托是一种通过将实际工作委托给其他对象来实现代码重用的机制。在Kotlin中,委托通过关键字by来实现......
  • Python3 协程 await async 相关的用法和笔记
    想要提供可以进行协程切换的awaitable,可以使用下面的方法:1任务taskasyncdeffunc():print("yesWait")task=asyncio.create_task(func())awaittask2协程对象,可以使asyncdef定义的协程函数(是否能触发切换不一定,要看函数内容)函数内可以利用asyncio.sl......
  • Unity-协程
    Unity-协程协程的简单实现​ 一般的程序执行都是线性的,也就是必须一行一行的执行代码。​ 使用Unity提供的协程,就可以类似于开辟另一条线程,调整根据你所写的代码,调整下一行代码执行的时间。项目示例​ 下面的例子是一个U3DDemo中的代码,实现最简单的Enemy追击Player的......
  • Kotlin Notes - 1
    AclassinKotlinhasaprimaryconstructorandpossiblyoneormoresecondaryconstructors.//primaryconstructorclassPerson(valname:String){valchildren:MutableList<Person>=mutableListOf()//secondaryconstructorconstru......
  • kotlin 内联函数 inline
    一、当函数被声明为内联函数(函数的前缀增加inline),那么函数体会被直接替换到函数被声明的地方,而不是被正常的调用。如下的代码inlinefunsynchronized(lock:Lock,action:()->Unit){lock.lock()try{returnaction()}finally{lock.unlo......
  • kotlin 高阶函数
    一、定义:以另一个函数作为参数或者返回值的函数1、kotlin中,函数以lambda或者函数引用来表示 二、函数类型1、如下是函数的类型上述声明了函数的类型,括号内包含了该函数类型需要传入的参数类型,紧接着箭头,最后是返回的类型(在声明函数类型时候,返回类型即使是Unit也不可以省略)......
  • 基于Golang协程实现流量统计系统项目开发
    基于Golang协程实现流量统计系统项目开发上一节课我们已经架设好了一个网站。,但是因为我们的网站没有流量。也生成不了大量的日志,靠我们自己点击生成那点日志也不够测试的。所以这次我们就用GO语言批量生成我们想要的日志。好了。我们开始写代码我用的IDE工具是GOLAND,没有为......
  • GO实现分布式爬虫—掌握go语言通道与协程项目架构设计
    GO实现分布式爬虫—掌握go语言通道与协程项目架构设计Go高并发微服务分布式 1.命令行的用户管理 用户信息存储        =>内存        =>结构[]map        =>用户IDnameageteladdr            [len][]map......