首页 > 其他分享 >【客户端学习】Kotlin 协程的基本概念及用法

【客户端学习】Kotlin 协程的基本概念及用法

时间:2023-05-24 15:55:32浏览次数:42  
标签:协程 launch Kotlin 线程 IO 方法 Dispatchers 客户端

协程是什么?

协程是一种编程思想,并不局限于特定的语言。除 Kotlin 以外,其他的一些语言,如 Go、Python 等都可以在语言层面上实现协程。
Kotlin Coroutine 本质上是 Kotlin 官方提供的一套线程封装 API,其设计初衷是为了解决并发问题,让「协作式多任务」实现起来更方便。

协程与线程的关系

从 Android 开发者的角度去理解它们之间的关系:

  • 我们所有的代码跑在线程中,而线程跑在进程中
  • 协程也是跑在线程中的,可以是单线程,也可以是多线程
  • 单线程中,协程的总执行时间并不会比不用协程少
  • Android 系统上,在主线程上进行耗时操作(如网络请求),即使用了协程,也需要切换线程

协程的基本使用

使用 launch 方法

协程在写法上和普通的顺序代码类似,可以让开发者用同步的方式写出异步的代码。创建协程可以使用以下三种方式:

runBlocking {
    // 方法1:使用 runBlocking 顶层函数
}

GlobalScope.launch {
    // 方法2:使用 GlobalScope 单例对象,调用 launch 开启协程
}

val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    // 方法3:自行通过 CoroutineContext 创建一个 CoroutineScope 对象
}
  • 方法 1 适用于单元测试场景,实际开发中不使用,因为它是线程阻塞的;
  • 方法 2 与 runBlocking 相比不会阻塞线程,但它的生命周期会和 APP 一致,且无法取消;
  • 方法 3 比较推荐使用,可以通过 context 参数去管理和控制协程的生命周期。

此处的 launch 方法含义是:创建一个新的协程,并在指定的线程上运行它。传给 launch 方法的连续代码段就被叫做一个协程,传给 launch 方法的方法参数可以用于指定执行这段代码的线程。

coroutineScope.launch(Dispatchers.IO) {
    // 可以通过 Dispatchers.IO 参数把任务切到 IO 线程执行
}

coroutineScope.launch(Dispatchers.Main) {
    // 也可以通过 Dispatchers.Main 参数切换到主线程
}

使用 withContext 方法

这个方法可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切换回去继续执行,如下所示:

coroutineScope.launch(Dispatchers.Main) {        // 在 UI 线程开始
    val image = withContext(Dispatchers.IO) {    // 切换到 IO 线程
        getImage(imageId)                        // 在 IO 线程执行
    }
    imageView.setImageBitmap(image)              // 回到 UI 线程更新 UI
}

该方法支持自动切回原来的线程,能够消除并发代码在协作时产生的嵌套。如果需要频繁地进行线程切换,这种写法将有很大的优势,这就是「使用同步的方式写异步代码」。

使用 suspend 关键字

我们可以把 withContext 单独放进一个方法里面,此方法需要使用 suspend 关键字标记才能编译通过:

suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {}

使用 launch、async 等方法创建的协程,在执行到某个 suspend 方法时会从正在执行它的线程上脱离。互相脱离后的线程和协程将会分别执行不同的任务:

  • 线程:线程执行到了 suspend 方法,就暂时不再执行剩余协程代码,跳出协程的代码块。如果它是一个后台线程,它会被系统回收或者再利用(继续执行别的后台任务),与 Java 线程池中的线程等同;如果它是 Android 主线程,它会继续执行界面刷新任务。
  • 协程:协程会从上面被挂起的 suspend 方法开始,在该方法的参数指定的线程(如 Dispatchers.IO 所指定的 IO 线程)中继续往下执行。suspend 方法执行完成之后,会重新切换回它原先的线程。这个「切回来」的动作,在 Kotlin 中叫做 resume。

suspend 关键字只是一个提醒,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 方法。该关键字本身只有一个效果:限制这个方法只能在协程里或者另一个 suspend 方法中被调用,否则就会编译不通过。

获取协程的返回值

协程是一种异步的概念,需要一些特殊操作才能获取返回值。获取协程的返回值可以使用以下方式:

async / await

主要流程是使用 async 开启协程,然后调用 async 返回的 Deferred 对象的 await 方法获取协程运算的结果:

coroutineScope.launch(Dispatchers.IO) {
    val job = async {
        delay(1000)
        return@async "return value"
    }
    println("async result=${job.await()}")
}

suspendCoroutine

与 async 不同,suspendCoroutine 只是一个挂起方法,无法开启协程,需要在其他协程作用域中使用。协程运行结束后,使用 resume 提交返回值或使用 resumeWithException 抛出异常。

coroutineScope.launch(Dispatchers.IO) {
    try {
        val result = suspendCoroutine<String> {
            delay(1000)
            val random = Random().nextBoolean()
            if (random) {
                it.resume("return value")
            } else {
                it.resumeWithException(Exception("Coroutine Failure"))
            }
        }
        println("suspendCoroutine success result: $result")
    } catch (e: java.lang.Exception) {
        println("suspendCoroutine failure exception: $e")
    }
}

协程的非阻塞式挂起

「非阻塞式挂起」指的就是协程在挂起的同时切线程这件事情。使用了协程的代码看似阻塞,但由于协程内部做了很多工作(包括自动切换线程),它实际上是非阻塞的。在代码执行的过程中,线程虽然会切换,但写法上类似普通的单线程代码。
在 Kotlin 中,协程就是基于线程来实现的一种更上层的工具 API,类似于 Android 自带的 Handler 系列 API。在设计思想上,协程是一个基于线程的上层框架。Kotlin 协程并没有脱离 Kotlin 或者 JVM 创造新的东西,只是简化了多线程的开发。

代码示例

使用协程模拟实现一个网络请求,等待时显示 Loading,请求成功或者出错让 Loading 消失,并将状态反馈给用户。
在 ViewModel 中编写如下业务逻辑代码:

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
    enum class RequestStatus {
        IDLE, LOADING, SUCCESS, FAIL
    }
    
    val requestStatus = MutableStateFlow(RequestStatus.IDLE)
    
    /**
     * 模拟网络请求,并将状态设置给 requestStatus 变量
     */
    fun simulateNetworkRequest() {
        requestStatus.value = RequestStatus.LOADING
        viewModelScope.launch {
            val requestResult = async { performSimulatedRequest() }.await()
            requestStatus.value = if (requestResult) RequestStatus.SUCCESS else RequestStatus.FAIL
        }
    }
    
    /**
     * 使用 delay 方法模拟耗时操作,用随机数模拟请求成功或失败
     */
    private suspend fun performSimulatedRequest() = withContext(Dispatchers.IO) {
        delay(500)
        val random = Random()
        return@withContext random.nextBoolean()
    }
}

MainActivity 中使用 Jetpack Compose,将请求状态实时显示在界面上:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    val requestStatusState = mainViewModel.requestStatus.collectAsState()
                    val requestStatus by rememberSaveable { requestStatusState }
                    
                    Text(
                        text = requestStatus.name,
                        color = Color.Red
                    )
                }
            }
        }
        mainViewModel.simulateNetworkRequest()
    }
}

标签:协程,launch,Kotlin,线程,IO,方法,Dispatchers,客户端
From: https://www.cnblogs.com/Akatsuki-Sanjou/p/17428562.html

相关文章

  • Kotlin中的无符号数据类型
    无符号数据类型Kotlin支持了几种常见的无符号整型,如下表所示:数据类型数据大小取值范围UByte1字节0~255UShort2字节0~65535UInt4字节0~2^32-1ULong8字节0~2^64-1除此之外,还支持了对应无符号整型的数组类型:UByteArrayUShortArrayUIntAr......
  • Kotlin学习 01
    之前因为工作没有转型kotlin的必要,并且一些杂事较多,一直没有好好深入学习kotlin。现在放眼看去,kotlin已经是统治地位了,不会kotlin,真不好意思说自己会做安卓开发。非常遗憾,现在重新开始,我任是一个爱学新技术的追风少年。我的学习方法是:看语法讲解看代码修改代码或者自己实现......
  • git-lfs 客户端安装
    curl-shttps://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh|sudobashyuminstallgit-lfs参考地址:https://github.com/git-lfs/git-lfs/blob/main/INSTALLING.md......
  • SQLite3 客户端程序,Win32 SDK ,C/C++
    1 WIn32SDK程序,尽量放在一个文件中,主要用到Tree,ListView,Edit控件。2 把控件封装成类,但不封装窗口回调函数。类实例为全局变量,方便消息回调函数调用执行。这样做最简单。3 Edit控件处理按键输入,模仿shell执行SQL查询4打开数据库文件时若没有此文件则新建,否则打开,打开......
  • Abp Vnext 动态(静态)API客户端源码解析
    根据以往的经验,通过接口远程调用服务的原理大致如下:服务端:根据接口定义方法的签名生成路由,并暴露Api。客户端:根据接口定义方法的签名生成请求,通过HTTPClient调用。这种经验可以用来理解ABPVNext自动API的方式,但如果不使用自动API并且控制器定义了路由的情况下,远程调用的路......
  • 使用 @GrpcClient 实现客户端
    转载请注明出处:@GrpcClient注解的作用是将gRPC客户端注入到Spring容器中,方便在应用程序中使用gRPC客户端调用gRPC服务提供的函数。使用@GrpcClient注解,我们可以像使用其他SpringBean一样来使用gRPC客户端,无需手动创建连接通道和stub类对象,SpringBoot会自......
  • 03、Etcd 客户端常用命令
    上一讲我们安装etcd服务端,这一讲我们来一起学学如何使用etcd客户端常见的命令。文章内容来源于参考资料,如若侵权,请联系删除,谢谢。etcd可通过客户端命令行工具etcdctl对etcd进行请求操作#帮助命令,会列出所有的命令和选项,在记不太清命令的时候,可以使用etcdctl‐h#......
  • Docker 启动 [email protected] 并使用 Navicat 客户端连接
    docker运行mysql镜像dockerrun--namesome-mysql-p3306:3306-eMYSQL_ROOT_PASSWORD=my-secret-pw-dmysql:5.7其中some-mysql是您要分配给容器的名称my-secret-pw是要为MySQLroot用户设置的密码-p将容器的3306端口发布到主机的端口3306-e设置容器的环境......
  • oracle客户端配置
    只使用SQL*Plus,则下载以下两个包即可:  a)、InstantClientPackage-Basic:  AllfilesrequiredtorunOCI,OCCI,andJDBC-OCIapplications  instantclient-basic-win32-11.1.0.6.0.zip(43,316,697bytes)  b)、InstantClientPackage-SQL*Plus: ......
  • 请在微信客户端打开链接:火狐浏览器模拟微信浏览器内核教程 ,用chrome模拟微信浏览器访
    背景:微信浏览器能访问,出现,请在微信客户端打开链接。useragentswitcherforfirefox:https://addons.mozilla.org/en-US/firefox/addon/user-agent-switcher/FF越来越不行了,下载都下载不下来~useragentswitcher1.0.34中文版forchrome:http://www.pc6.com/soft/FireFox_133175.h......