kotlin协程的简述和使用

本文探讨了协程的概念及其解决异步编程难题的作用,对比了协程与线程的区别,并通过Kotlin语言展示了如何利用协程简化异步任务处理。

为什么会有协程 && 什么是协程

当我们最初学习程序之时,我们书写代码、使用指令执行,完成逻辑链条的前后关系。代码执行到哪,逻辑就走到哪。但问题随之出现,有些过程并不是能立即得到结果的,监听按钮或者一些耗时操作如IO操作等,程序为了等待结果就会阻塞。在一些应用场景之下,我们常常会使用异步api,通过一些回调函数操作来完成异步任务。

但是异步回调也有本身的问题,第一是,原本的统一的同步逻辑被拆分成几个阶段,造成代码可读性不好。第二是,在复杂的应用场景下,可能造成十分难受的回调地狱。以及难以debug的问题。

// JavaScript展示地狱回调
setTimeout(function () {  // 第一层
    console.log('第一层'); // 等3秒打印,再执行下一个回调函数
    setTimeout(function () {  // 第二层
        console.log('第二层'); // 等2秒打印,再执行下一个回调函数
        setTimeout(function () {   // 第三层
            console.log('第三层'); // 等1秒打印
        }, 1000)
    }, 2000)
}, 3000)

协程则应运而生。协程就是协作式多任务,协程本身是一种任务调度机制,也可以说是一种编程设计思想并不限定语言(在1958年就被发明,并用于构建汇编程序)。协程可以使用同步的代码逻辑流去操作异步的控制流。并且不会导致在操作系统的线程阻塞。相对于线程之间的切换,协程是更轻量级的。对于线程的操作是调用了操作系统的功能,而启用协程则是编程语言来完成,所以协程也被称为用户态线程。

协程为并发而生,线程在CPU多核之下,已然能完成并行。如图所示,并发本身是对CPU时间片的争夺。而对于异步任务进行线程切换,这才是协程能做的事。下图为CPU时间片段对于串行、并行、并发的线程处理的区别。

协程对于异步任务是主动让出而不是抢占的多任务,突出是主动让出;而线程则是抢占式的多任务,突出被动抢占。

抢占是一个相对低效的操作,“打断”这种操作不是那么容易做的,操作系统级别上之所以需要抢占是为了避免任务占着CPU不走。但在你自己知根知底的代码还要去抢占,这基本上就是牺牲性能的操作。所以使用协程避免抢占可以提高性能。

部分语言对于协程的支持

  • go语言的协程,直接在语法层面支持协程。解决了服务端开发中,IO密集型任务,并发性能程序过于复杂的痛点。go可以很优雅地进行高并发场景的开发。go语言的协程叫 Goroutines,从英文拼写就知道它和 Coroutines 还是有些差别的(设计思想上是有关系的),不然Kotlin的协程完全可以叫 Koroutines。
  • 之前Java对于协程而言,可以使用NIO(new IO)和一些多线程api进行操作,能模拟出一定的协程的效果,但实际开发方面还是过于麻烦,也许go语言在服务器端异军突起,在协程并发方面Java落于下风不无关系。
  • 2022年9月份,Oracle正式发布了最新版本的Java19和对应的Java虚拟线程的特性。是为帮助提高大型服务器的应用性能。Java虚拟线程在设计思想上,就是轻量级线程。值得关注的是,这次改动对于Java各个Api的改动很少,并没有太多新语法。Java19的虚拟线程是预览特性,很可能在Java21才会成为正式特性。
  • 很多语言都有自己的协程(虚拟线程)的技术,除了Go、Java,还有C#、Erlang、Lua等等。

kotlin协程

协程概念本身与线程概念是同一级别的东西。

在kotlin-JVM、Android平台中,对于协程的运用本质成为了切换线程,在性能上本身不会比优化后的线程池强。这是kotlin-JVM语言对于协程的实现。甚至在其他平台,kotlin-native、kotlin-javascript上,协程的实现的方法都完全不一样。在这个基础上,可以说,协程未必等于切线程,而未必就不能强于线程池。只是kotlin-JVM的协程实现就是基于切换线程。

kotlin协程写法

kotlin协程的写法和运用方案很多,有:runBlocking顶层函数;GlobalScope(CoroutineScope)单例对象调用launch开启协程;创建一个 CoroutineScope 对象开启协程等等。

这里我主要讲比较简单的开启协程的方法。

CoroutineScope(Dispatchers.Main).launch {
    // your code
    
    withContext(Dispatchers.IO) {
        // your blocking code
    }

    // your code

    withContext(Dispatchers.IO) {
        // your blocking code
    }
    
    // your code
    
}

如上的写法就是在main线程中进行异步操作,将阻塞和耗时操作放入withContext的切换线程的代码块里,kotlin程序就自然帮我们完成了线程之间的切换,以同步的写法完成异步的事情,这就是协程。

如下的代码是实现一个计时器更新主线程button的文字的功能,是kotlin使用协程对比使用线程池,kotlin-JVM协程在写法和性能上不一定更优秀,这些都是设计思想和方案选择的碰撞和抉择。如下代码,进行了简单的时间增加并在Android的UI线程更新的逻辑。

private fun useCoroutines() {
    CoroutineScope(Dispatchers.Main).launch {
        buttonCount.isEnabled = false
        var count = 0
        while (count < 10) {
            withContext(Dispatchers.IO) {
                delay(SECOND_DURATION)
                count++
            }
            buttonCount.text = String.format(
                Locale.getDefault(),
                "%d",
                count
            )
        }
        buttonCount.isEnabled = true
    }
}
private fun useExecutor() {
    Executors.newSingleThreadExecutor().execute {
        var count = 0
        while (count < 10) {
            SystemClock.sleep(SECOND_DURATION)
            count++
            val finalCount = count
            runOnUiThread {
                buttonCount.text = String.format(
                    Locale.getDefault(),
                    "%d",
                    finalCount
                )
            }
        }
        runOnUiThread { buttonCount.isEnabled = true }
    }
}

suspend关键字

suspend 是 Kotlin 协程最核心的关键字之一。官方解释是,代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。

什么是挂起函数

suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或者其他挂起函数内使用。协程内部挂起函数的调用处被称为挂起点,也就是Android Studio代码左边会出现的一个箭头加一个绿色波浪线的标志。

网上寻到的gif例子,具体挂起函数演示如下:

getUserInfo()、getFriendList(user)、getFeedList(friendList)三个函数都是挂起函数。内部必然为 suspend修饰的挂起函数。例如getUserInfo函数可为下内容:

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) { // 切换到IO线程
        delay(1000L) // 延迟1s
    }
    return "content"
}

挂起的对象是协程,挂起的本质“就是这个协程从正在执行它的线程上脱离”。注意,这里不是这个协程停下来,而是脱离,从当前线程脱离,比如上文的代码,就是从主线程脱离,去IO或工作线程上进行处理。紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。如上文所示,我们代码本身在主线程允许,当协程切走的函数执行完毕,协程会帮助我们post一个Runnable,让我们剩下的代码和信息继续回到之前的线程去运行。

参考文献

<think>首先,用户的问题是:“Kotlin协程与线程的区别,面试时应该怎么回答”。我需要用中文回答这个问题。 我的回答必须包括: 1. 对问题的完整回答。 2. 在回答的末尾,必须提出3-5个相关问题。 3. 相关问题必须以“§§相关问题§§”开头,并且是第一行,不能换行分散。 4. 相关问题要基于用户的问题我的回答。 问题核心:Kotlin协程与线程的区别,以及如何在面试中回答。 关键点: - **协程(Coroutines)**:在Kotlin中,协程是轻量级的线程,用于异步编程。它们不直接映射到操作系统线程;而是由Kotlin运行时管理,可以挂起恢复,减少上下文切换开销。 - **线程(Threads)**:操作系统级别的线程,每个线程有自己的栈上下文。创建切换线程开销较大。 - **区别**: - **资源开销**:协程更轻量,可以创建数千个协程而不会显著影响性能;线程开销大。 - **并发模型**:协程使用协作式调度(需要显式挂起),线程使用抢占式调度。 - **阻塞与非阻塞**:协程挂起时不阻塞线程,线程阻塞时会浪费资源。 - **错误处理**:协程有结构化的并发机制,如作用域取消。 - **上下文切换**:协程切换在用户空间,高效;线程切换在内核空间,开销大。 - **面试回答建议**:结构化、清晰。例如: - 定义两者。 - 对比关键差异。 - 举例说明在Kotlin中的使用。 - 强调协程的优点:如简化异步代码、避免回调地狱。 我的回答结构: 1. 先解释协程线程的基本概念。 2. 详细列出区别。 3. 给出面试回答的建议。 4. 最后,添加相关问题。 相关问题:必须3-5个,基于问题我的回答。例如: 1. 协程在哪些场景下比线程更适用? 2. 如何在Kotlin中启动一个协程? 3. 协程的挂起函数是如何工作的? 4. 线程池协程调度器有什么区别? 5. 协程如何处理异常? 确保相关问题以“§§相关问题§§”开头,然后列出编号列表。 最后,输出格式:回答内容后,直接以“§§相关问题§§”开头,然后列出问题。 完整回答草稿: - 开头:解释问题。 - 主体:区别面试建议。 - 结尾:相关问题。 相关问题数量:3-5个。我选择4个。 最终输出: - 回答内容。 - §§相关问题§§ 1. ... 2. ... 3. ... 4. ...</think>### Kotlin协程与线程的区别及面试回答指南 在Kotlin中,协程(Coroutines)线程(Threads)都是处理并发编程的工具,但它们在设计理念、资源开销使用场景上有显著差异。以下是核心区别及面试回答建议: #### 核心区别: 1. **资源开销**: - **线程**:是操作系统级别的实体,每个线程需要独立的栈内存(通常1MB左右)内核调度。创建切换线程开销大,过多线程会导致内存CPU瓶颈。 - **协程**:是用户级别的轻量级“虚拟线程”,由Kotlin运行时管理(不依赖OS)。协程共享线程资源,挂起时仅保存少量状态(如局部变量),可创建数千个协程而几乎无性能损失。例如,一个线程上可运行多个协程。 2. **调度方式**: - **线程**:使用抢占式调度(操作系统强制切换),可能导致资源争用死锁。 - **协程**:使用协作式调度(需显式调用`suspend`函数挂起),开发者控制挂起点,避免不必要的上下文切换,更高效。 3. **阻塞行为**: - **线程**:阻塞操作(如I/O)会占用整个线程,浪费资源。 - **协程**:挂起操作(如`delay()`)是非阻塞的,释放底层线程给其他协程使用,提高资源利用率。 4. **并发模型**: - **线程**:基于共享内存锁(易引发竞态条件)。 - **协程**:支持结构化并发(通过`CoroutineScope``Job`),提供取消、异常传播等机制,代码更安全易维护。 5. **使用场景**: - **线程**:适合CPU密集型任务或需直接操作硬件。 - **协程**:适合I/O密集型、异步任务(如网络请求),简化回调地狱。 #### 面试回答建议: - **结构化回答**: 1. **定义**:先简述线程是OS资源,协程Kotlin的轻量级并发工具。 2. **对比关键点**:强调开销、调度、阻塞行为(如“协程挂起不阻塞线程,线程阻塞会浪费资源”)。 3. **举例**:结合Kotlin代码,如使用`launch`启动协程 vs `Thread.start()`。 4. **优势总结**:突出协程的简洁性(如用`suspend`函数替代回调)高效性。 5. **场景适配**:说明协程更适合Android/后端异步编程,线程用于底层优化。 - **避免误区**:不提“协程是线程的替代”,而是互补工具;面试时可问“您项目中如何选择?”以示深度。 - **示例代码**: ```kotlin // 协程示例 fun main() = runBlocking { launch { delay(1000) // 挂起不阻塞线程 println("Coroutine done") } } // 线程示例 fun main() { Thread { Thread.sleep(1000) // 阻塞线程 println("Thread done") }.start() } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值