0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

协程的概念及协程的挂起函数介绍

jf_78858299 来源:labuladong 作者:labuladong 2023-04-19 10:20 次阅读

什么是协程

协程是一种轻量级的线程,它可以在单个线程中实现并发执行。与线程不同,协程不需要操作系统的上下文切换,因此可以更高效地使用系统资源。Kotlin 协程是 Kotlin 语言的一项特性,它提供了一种简单而强大的方式来处理异步任务。

相关的基本概念

挂起函数

挂起函数是一种特殊的函数,它可以在执行过程中暂停并等待某些操作完成。在 Kotlin 中,挂起函数使用 suspend 关键字进行标记。挂起函数的特点是可以在函数内部使用 suspend 关键字标记的其他挂起函数,这些挂起函数会在执行过程中暂停当前协程的执行,并等待异步任务的结果。当异步任务完成后,协程会自动恢复执行,并将结果返回给调用方。

以下是一个使用挂起函数的例子,该例子使用 Retrofit 库进行网络请求:

suspend fun fetchUser(userId: String): User {
    return withContext(Dispatchers.IO) {
        // 创建 Retrofit 实例
        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        // 创建 API 接口
        val apiService = retrofit.create(ApiService::class.java)
        // 发起网络请求
        val response = apiService.getUser(userId)
        // 解析响应
        val user = response.body()
        // 返回结果
        user ?: throw IllegalStateException("User not found")
    }
}

在上面的例子中,fetchUser 函数使用了 withContext 函数来切换到 IO 线程执行网络请求。在网络请求的过程中,使用了 Retrofit 库提供的挂起函数 getUser 来发起网络请求,并等待响应结果。当响应结果返回后,协程会自动恢复执行,并将结果返回给调用方。

需要注意的是,挂起函数只能在协程中使用,不能在普通的函数中使用。在使用挂起函数时,我们需要将其包装在协程作用域中,以便管理协程的生命周期。例如:

val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
    val user = fetchUser("123")
    // 处理用户数据
}
scope.cancel()

在上面的例子中,我们使用了协程作用域来管理协程的生命周期。在协程作用域中,我们使用 launch 函数来启动一个新的协程,并在其中调用 fetchUser 函数来获取用户数据。当协程作用域结束时,协程会自动取消,避免了线程泄漏的问题。

协程作用域

协程作用域是一种管理协程的机制,它可以确保协程在指定的作用域内运行,并在作用域结束时自动取消协程。在 Kotlin 中,协程作用域由 CoroutineScope 接口表示。

协程作用域的主要作用是管理协程的生命周期。在协程作用域内启动的协程会自动继承作用域的上下文和调度器,并在作用域结束时自动取消。这样,我们就可以避免协程泄漏和线程泄漏的问题,提高程序的性能和稳定性。

协程作用域还可以将多个协程组合在一起,实现并发执行。在协程作用域中,我们可以使用 async 函数来启动一个新的协程,并返回一个 Deferred 对象,该对象可以用于获取协程的执行结果。例如:

val scope = CoroutineScope(Dispatchers.IO)
val deferred1 = scope.async { fetchUser("123") }
val deferred2 = scope.async { fetchUser("456") }
val users = listOf(deferred1.await(), deferred2.await())
scope.cancel()

在上面的例子中,我们使用协程作用域来管理两个协程的生命周期,并使用 async 函数来启动两个协程,分别获取用户数据。在获取用户数据的过程中,我们使用了 await 函数来等待协程的执行结果。当两个协程都执行完成后,我们将结果保存到 users 列表中。

❝需要注意的是,协程作用域是一种轻量级的机制,它不会创建新的线程或进程。协程作用域中的协程会在当前线程中执行,并使用协程调度器来管理协程的执行。因此,我们需要根据具体的需求选择合适的协程调度器,以便实现最佳的性能和响应速度。

Dispatchers.IO 是 Kotlin 协程库中的一个协程调度器,它用于将协程分配到 IO 线程池中执行。在协程中执行 IO 操作时,我们通常会使用 Dispatchers.IO 调度器来避免阻塞主线程或其他重要线程。

Android 应用程序中,主线程通常用于处理 UI 事件和更新 UI 界面,因此我们应该尽量避免在主线程中执行耗时的 IO 操作。如果我们在主线程中执行耗时的 IO 操作,会导致 UI 界面卡顿或无响应,影响用户体验。为了避免在主线程中执行耗时的 IO 操作,我们可以使用 Dispatchers.IO 调度器将协程分配到 IO 线程池中执行。IO 线程池通常包含多个线程,用于执行网络请求、文件读写、数据库操作等耗时的 IO 操作。在 IO 线程池中执行 IO 操作时,我们可以使用挂起函数来等待异步操作的完成,而不需要阻塞主线程或其他重要线程。

例如,在下面的例子中,我们使用 Dispatchers.IO 调度器来将协程分配到 IO 线程池中执行网络请求:

val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
    val response = fetchUser("123")
    // 处理响应结果
}
scope.cancel()

在上面的例子中,我们使用 launch 函数启动了一个新的协程,并使用 Dispatchers.IO 调度器将其分配到 IO 线程池中执行。在协程中,我们使用 fetchUser 函数来发起网络请求,并使用挂起函数来等待响应结果的返回。当响应结果返回后,协程会自动恢复执行,并将结果返回给调用方。

在 Kotlin 中,我们可以使用 CoroutineScope 接口来创建协程作用域,并在作用域内启动协程。在创建协程作用域时,我们需要指定协程的上下文和调度器,以便管理协程的生命周期和执行。

  • 使用 GlobalScope GlobalScope 适用于一些简单的、短时间的任务,例如发送一条日志、执行一个简单的计算等。由于 GlobalScope 是一个全局的协程作用域,因此这种方式不适合长时间运行的任务,因为它可能会导致协程泄漏和线程泄漏的问题。
GlobalScope.launch {
    // 发送一条日志
    Log.d(TAG, "Hello, World!")
}
  • 使用 CoroutineScope CoroutineScope 适用于一些需要长时间运行的任务,例如网络请求、文件读写、数据库操作等。在创建协程作用域时,我们需要指定协程的上下文和调度器,以便管理协程的生命周期和执行。
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
    // 执行一个网络请求
    val response = fetchUser("123")
    // 处理响应结果
}

在上面的例子中,我们使用 CoroutineScope 创建了一个局部的协程作用域,并使用 Dispatchers.IO 调度器将协程分配到 IO 线程池中执行。在协程中,我们使用 fetchUser 函数来发起网络请求,并使用挂起函数来等待响应结果的返回。当响应结果返回后,协程会自动恢复执行,并将结果返回给调用方。

  • runBlocking runBlocking 适用于一些测试代码,例如单元测试、集成测试等。在测试代码中,我们通常需要启动协程,并等待协程执行完成后进行断言。
@Test
fun testFetchUser() = runBlocking {
    // 启动一个协程
    val response = fetchUser("123")
    // 断言响应结果
    assertEquals("John Doe", response.name)
}

在上面的例子中,我们使用 runBlocking 启动了一个新的协程,并在协程中发起了一个网络请求。由于这是一个测试代码,因此我们可以使用 runBlocking 阻塞当前线程,直到协程执行完成后进行断言。

  • lifecycleScope lifecycleScope 适用于一些需要与 Activity 或 Fragment 的生命周期绑定的任务,例如更新 UI 界面、执行后台任务等。在使用 lifecycleScope 时,我们可以避免协程泄漏和线程泄漏的问题,并且可以自动取消协程,以便释放资源。
class MyFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycleScope.launch {
            // 更新 UI 界面
            textView.text = "Hello, World!"
            // 执行后台任务
            val response = fetchUser("123")
            // 处理响应结果
        }
    }
}

在上面的例子中,我们在 Fragment 的 onViewCreated 方法中使用 lifecycleScope 启动了一个新的协程,并将其与 Fragment 的生命周期绑定。当 Fragment 被销毁时,lifecycleScope 会自动取消协程,以便释放资源。在协程中,我们可以更新 UI 界面、执行后台任务等操作,而不需要担心协程泄漏和线程泄漏的问题。

协程调度器

协程调度器是一种决定协程在哪个线程上运行的机制。在 Kotlin 中,协程调度器由 CoroutineDispatcher 接口表示。

常用的调度器如下

  • Dispatchers.Default:将协程分配到默认的线程池中执行。默认的线程池通常包含多个线程,用于执行 CPU 密集型的计算任务。
  • Dispatchers.IO:将协程分配到 IO 线程池中执行。IO 线程池通常包含多个线程,用于执行网络请求、文件读写、数据库操作等耗时的 IO 操作。
  • Dispatchers.Main:将协程分配到主线程中执行。主线程通常用于处理 UI 事件和更新 UI 界面。
  • Dispatchers.Unconfined:将协程分配到当前线程中执行,直到第一个挂起点。在第一个挂起点之后,协程会自动切换到其他线程或线程池中执行。

❝除了上述常用的调度器之外,我们还可以自定义调度器,以便更好地满足具体的需求。例如,我们可以使用 newSingleThreadContext 函数创建一个新的单线程调度器,用于将协程分配到单个线程中执行。

协程上下文

协程上下文是一组键值对,它包含了协程的一些属性和配置信息。在 Kotlin 中,协程上下文由 CoroutineContext 接口表示。

在 Kotlin 协程中,协程上下文(Coroutine Context)是一个包含了协程执行所需的各种元素的对象。协程上下文可以包含多个元素,例如调度器、异常处理器、协程名称等。在协程中,我们可以使用 coroutineContext 属性来访问当前协程的上下文。

以下是协程上下文中常用的元素:

  • Job:协程的任务,用于管理协程的生命周期和取消操作。
  • CoroutineDispatcher:协程的调度器,用于将协程分配到不同的线程或线程池中执行。
  • CoroutineExceptionHandler:协程的异常处理器,用于处理协程中发生的异常。
  • CoroutineName:协程的名称,用于标识协程的作用和用途。

在协程中,我们可以使用 CoroutineScope 接口来创建协程作用域,并在作用域内启动协程。在创建协程作用域时,我们可以指定协程的上下文和调度器,以便管理协程的生命周期和执行。

在协程中,我们可以使用 withContext 函数来切换协程的上下文和调度器。withContext 函数会挂起当前协程,并在指定的上下文和调度器中启动一个新的协程。当新的协程执行完成后,withContext 函数会自动恢复当前协程的执行。

以下是使用 withContext 函数切换协程上下文的示例:

suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
    // 在 IO 线程池中执行网络请求
    val response = apiService.fetchUser(id)
    // 解析响应结果
    val user = response.toUser()
    // 返回用户信息
    user
}

在上面的例子中,我们使用 withContext函数将协程的上下文切换到 Dispatchers.IO 调度器中,并在 IO 线程池中执行网络请求。当网络请求完成后,withContext 函数会自动恢复当前协程的执行,并将解析后的用户信息返回给调用方。

除了使用 withContext 函数切换协程上下文外,我们还可以使用 CoroutineScope 接口的扩展函数来切换协程上下文。以下是使用 CoroutineScope 接口的扩展函数切换协程上下文的示例:

val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
    // 在主线程中执行 UI 操作
    textView.text = "Loading..."
    // 切换协程上下文到 IO 线程池中执行网络请求
    val user = withContext(Dispatchers.IO) {
        apiService.fetchUser("123")
    }
    // 切换协程上下文到主线程中更新 UI 界面
    textView.text = "Hello, ${user.name}!"
}

在上面的例子中,我们使用 CoroutineScope 创建了一个局部的协程作用域,并将其与主线程的调度器绑定。在协程中,我们使用 withContext 函数将协程的上下文切换到 IO 线程池中执行网络请求。当网络请求完成后,我们再次使用 withContext 函数将协程的上下文切换回主线程中更新 UI 界面。

最后

这篇文章主要介绍了协程的概念,协程的挂起函数,作用域,调度器和上下文,更多文章可以关注公众号QStack。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 操作系统
    +关注

    关注

    37

    文章

    6820

    浏览量

    123327
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19682
  • kotlin
    +关注

    关注

    0

    文章

    60

    浏览量

    4191
收藏 人收藏

    评论

    相关推荐

    谈谈的那些事儿

    随着异步编程的发展以及各种并发框架的普及,作为一种异步编程规范在各类语言中地位逐步提高。我们不单单会在自己的程序中使用,各类框架如fastapi,aiohttp等也都是基于异步
    的头像 发表于 01-26 11:36 1116次阅读
    谈谈<b class='flag-5'>协</b><b class='flag-5'>程</b>的那些事儿

    和线程有什么区别

    和线程的区别和线程的共同目的之一是实现系统资源的上下文调用,不过它们的实现层级不同;线程(Thraed)是比进程小一级的的运行单位,多线程实现系统资源上下文调用,是编程语言交付
    发表于 12-10 06:23

    什么是多任务系统?FreeRTOS任务与简析

    功能,初学者必须先掌握——任务的创建、删除、挂起和恢复等操作。本章节分为如下几部分:*什么是多任务系统*FreeRTOS任务与*初次使用*任务状态*任务优先级*任务实现*任务控制块*任务堆栈一、什么是多任务系统单片机一般都是
    发表于 02-18 06:38

    关于C++ 20最全面详解

    花了一两周的时间后,我想写写 C++20 的基本用法,因为 C++ 的让我感到很奇怪,写一个
    的头像 发表于 04-12 11:10 1.3w次阅读
    关于C++ 20<b class='flag-5'>协</b><b class='flag-5'>程</b>最全面详解

    Python后端项目的是什么

    最近公司 Python 后端项目进行重构,整个后端逻辑基本都变更为采用“异步”的方式实现。看着满屏幕经过 async await(在 Python 中的实现)修饰的代码,我顿时
    的头像 发表于 09-23 14:38 1331次阅读

    Python与JavaScript的对比及经验技巧

    前言以前没怎么接触前端,对 JavaScript 的异步操作不了解,现在有了点了解。一查发现 Python 和 JavaScript 的发展史简直就是一毛一样!这里大致做下横向对比和总结,便于
    的头像 发表于 10-20 14:30 1935次阅读

    通过例子由浅入深的理解yield

    send:send() 方法致使程前进到下一个yield 语句,另外,生成器可以作为使用
    的头像 发表于 08-23 11:12 2025次阅读

    使用channel控制数量

    goroutine 是轻量级线程,调度由 Go 运行时进行管理的。Go 语言的并发控制主要使用关键字 go 开启 goroutine。Go (Goroutine)之间通过信道(
    的头像 发表于 09-19 15:06 1139次阅读

    详解Linux线程、线程与异步编程、与异步

    不是系统级线程,很多时候被称为“轻量级线程”、“微线程”、“纤(fiber)”等。简单来说可以认为
    的头像 发表于 03-16 15:49 983次阅读

    Kotlin实战进阶之筑基篇3

    概念在1958年就开始出现(比线程还早), 目前很多语言开始原生支, Java 没有原生但是大型公司都自己或者使用第三方库来支持
    的头像 发表于 05-30 16:26 697次阅读

    FreeRTOS任务与介绍

    FreeRTOS 中应用既可以使用任务,也可以使用(Co-Routine),或者两者混合使用。但是任务和协使用不同的API函数,因此不能通过队列(或信号量)将数据从任务发送给
    的头像 发表于 09-28 11:02 994次阅读

    的作用、结构及原理

    本文介绍的作用、结构、原理,并使用C++和汇编实现了64位系统下的池。文章内容避免了
    的头像 发表于 11-08 16:39 1136次阅读
    <b class='flag-5'>协</b><b class='flag-5'>程</b>的作用、结构及原理

    C/C++编程的相关概念和技巧

    自己的寄存器上下文和栈,可以在多个入口点间自由切换,而不是像传统的函数调用那样在一个入口点开始、另一个入口点结束。概念最早可以追溯到1963年,由Melvin Conway提出。
    的头像 发表于 11-09 11:34 792次阅读

    的实现与原理

    前言 这个概念很久了,好多程序员是实现过这个组件的,网上关于的文章,博客,william hill官网 都是汗牛充栋,在知乎,github上面也有很多大牛写了
    的头像 发表于 11-10 10:57 438次阅读

    Linux线程、线程与异步编程、与异步介绍

    不是系统级线程,很多时候被称为“轻量级线程”、“微线程”、“纤(fiber)”等。简单来说可以认为
    的头像 发表于 11-11 11:35 1160次阅读
    Linux线程、线程与异步编程、<b class='flag-5'>协</b><b class='flag-5'>程</b>与异步<b class='flag-5'>介绍</b>