深潜Kotlin协程(六):协程构建器

2023-11-28 17:59
文章标签 构建 kotlin 协程 深潜

本文主要是介绍深潜Kotlin协程(六):协程构建器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

系列电子书:传送门


协程构建器

挂起函数需要相互传递 continuation。它们调用普通函数没有任何问题,但是普通函数不能调用挂起函数。

在这里插入图片描述
每个挂起函数都需要由另一个挂起函数调用,而另一个挂起函数又由另一个挂起函数调用,以此类推。但这一切都要从某个地方开始。这个地方就是协程构建器,它是连接正常世界和挂起世界的桥梁。

我们将探讨 kotlinx.continues 库提供的三个基本协程构建器:

  • launch
  • runBlocking
  • async

每种方法都有其用例,让我们来逐一了解。

launch

launch 工作的方式在概念上类似于启动一个新线程(thread 函数)。我们只需启动一个协程,它就会独立运行,就像向空中发射的烟花。下面就是我们如何使用 launch 来启动一个协程:

fun main() {GlobalScope.launch {delay(1000L)println("World!")}GlobalScope.launch {delay(1000L)println("World!")}GlobalScope.launch {delay(1000L)println("World!")}println("Hello,")Thread.sleep(2000L)
}
// Hello,
// (1 sec)
// World!
// World!
// World!

launch 函数是 CoroutineScope 接口上的一个扩展函数。它是一个被称为结构化并发机制的重要部分,其目的是在父协程和子协程之间建立关系。在本章的后面,我们将学习结构化并发,但现在我们将通过在 GlobalScope 对象上调用 launch (之后是 async)来避免这个概念。这并不是一个标准的实现,因为我们在实际项目中很少使用 GlobalScrope

你可能已经注意到的另一件事情:在 main 函数的末尾,我们需要调用 Thread.sleep ,如果不这样做,该函数将会在启动协程后立即结束,因此它们将没有机会完成它们的工作。这是因为 delay 不会阻塞线程:它只是挂起一个协程。你们还记得挂起是如何工作的么? delay 只是设置一个计时器,并挂起一个协程,在一段时间后恢复。如果线程没有被阻塞,那么没有什么事情是繁忙的,那就没有什么事情阻止程序完成(稍后我们将看到,如果我们使用结构化并发,就没有要调用 Thread.sleep)。

在某种程度上, launch 的工作原理类似 daemon 线程,但是它要廉价的多。维护一个阻塞的线程总是很昂贵的,而维护一个挂起的协程几乎是无成本的(正如前面介绍底层运作的章节中解释的那样)。它们都独立的启动了一些任务,并且需要一些东西来防止程序在它们完成之前结束(在下面例子中,是 Thread.sleep(2000L)):

fun main() {thread(isDaemon = true) {Thread.sleep(1000L)println("World!")}thread(isDaemon = true) {Thread.sleep(1000L)println("World!")}thread(isDaemon = true) {Thread.sleep(1000L)println("World!")}println("Hello,")Thread.sleep(2000L)
}

runBlocking

一般规则下协程不应该阻塞线程,而应该挂起它们,但另一方面,在某些情况下阻塞却是必要的。与 main 函数一样,我们需要阻塞线程,否则程序将过早的结束。对于这种情况,我们可以使用 runBlocking

runBlocking 是一个特殊的协程构建器。当它的协程被挂起时(类似于挂起 main),它会阻塞它已经启动的线程。这意味着 runBlocking 内部的 delay(1000L) 将表现的像 Thread.sleep(1000L) 一样。

fun main() {runBlocking {delay(1000L)println("World!")}runBlocking {delay(1000L)println("World!")}runBlocking {delay(1000L)println("World!")}println("Hello,")
}
// (1 sec)
// World!
// (1 sec)
// World!
// (1 sec)
// World!
// Hello,
fun main() {Thread.sleep(1000L)println("World!")Thread.sleep(1000L)println("World!")Thread.sleep(1000L)println("World!")println("Hello,")
}
// (1 sec)
// World!
// (1 sec)
// World!
// (1 sec)
// World!
// Hello,

实际上有几个使用 runBlocking 的特定场景,第一个是 main 函数,我们需要阻塞线程,否则程序将结束。另一个常见用例是单元测试,由于同样的原因,我们需要阻塞线程。

fun main() = runBlocking {// ...
}class MyTests {@Testfun `a test`() = runBlocking {}
}

在我们的例子中,我们可以使用 runBlockingThread.sleep(200) 替换为 delay(2000)。稍后我们将看到,一旦引入结构化并发,它将会变得更加有用。

fun main() = runBlocking {GlobalScope.launch {delay(1000L)println("World!")}GlobalScope.launch {delay(1000L)println("World!")}GlobalScope.launch {delay(1000L)println("World!")}println("Hello,")delay(2000L) // still needed
}
// Hello,
// (1 sec)
// World!
// World!
// World!

runBlocking 曾经是一个重要的构建器,但是在现代编程中很少使用它。在单元测试中,我们经常使用它的接班人 runTest,它使协程在虚拟时间内运行(这是一个非常有用的测试特性,我们将在协程测试一章中描述)。对于main函数,我们经常将其挂起(用 suspend 修饰),挂起 main 函数很方便,但现在之后我们将会使用 runBlocking 来运行它。

async

asynce 协程构建器类似于 launch,但它的设计目的是去产生一个值。这个值需要由 lambda 表达式来返回。 async 函数返回一个 Deferred<T> 类型的对象,其中 T 是生成值的类型。 Deferred 有一个挂起方法 await,当它准备好时会返回这个值。在下面的例子中,生成的值是42,它的类型是 Int,因此 async 返回 Deferred<Int> 类型, await 函数返回42的 Int 值。

fun main() = runBlocking {val resultDeferred: Deferred<Int> = GlobalScope.async {delay(1000L)42}// do other stuff...val result: Int = resultDeferred.await() // (1 sec)println(result) // 42// or justprintln(resultDeferred.await()) // 42
}

就像 launch 构建器一样,async 在被调用时,立马启动协程。因此,它是一种一次启动多个任务,然后等待所有结果的方法。返回的 Deferred 会将生成后的值将存储在自身内部,因此值一旦准备好了,它将立即从 await 返回。但是,如果在值产生之前调用 await,则该函数会被挂起,直到值准备好。

fun main() = runBlocking {val res1 = GlobalScope.async {delay(1000L)"Text 1"}val res2 = GlobalScope.async {delay(3000L)"Text 2"}val res3 = GlobalScope.async {delay(2000L)"Text 3"}println(res1.await())println(res2.await())println(res3.await())
}
// (1 sec)
// Text 1
// (2 sec)
// Text 2
// Text 3

async 构建器的工作方式与 launch 非常相似,但它对返回值有额外的支持。如果所有 launch 函数都用 async 来替代,代码仍然可以正常工作。但是请不要这样做! async 使用来生成值的,所以如果我们不需要值,我们仍应该使用 launch

fun main() = runBlocking {// 不要这么做GlobalScope.async {delay(1000L)println("World!")}println("Hello,")delay(2000L)
}
// Hello,
// (1 sec)
// World!

async 构建器通常用于并行两个任务,例如从两个不同的地方获取数据,并将它们组合在一起。

scope.launch {val news = async {newsRepo.getNews().sortedByDescending { it.date }}val newsSummary = newsRepo.getNewsSummary()view.showNews(newsSummary,news.await())
}

结构化并发

如果一个协程在 GlobalScope 上启动,程序不会等待它。如上所述,协程不会阻塞任何线程,也不会阻止程序结束,这就是为什么在下面这个例子中,如果我们想要看到 runBlocking 打印出 “World!”,需要在末尾调用额外的 delay 函数。

fun main() = runBlocking {GlobalScope.launch {delay(1000L)println("World!")}GlobalScope.launch {delay(2000L)println("World!")}println("Hello,")// delay(3000L)
}
// Hello,

我们为什么需要在开始的地方使用 GlobalScope?这是因为 launchasync 都是 CoroutineScope 上的扩展函数。但是,如果你查看这些函数和 runBlocking 的定义,你将看到 block 参数是一个函数类型,其接收类型都是 CoroutineScope

fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext,block: suspend CoroutineScope.() -> T
): Tfun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit
): Jobfun <T> CoroutineScope.async(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> T
): Deferred<T>

这意味着我们可以摆脱 GlobalScope,作为替代,可以用 runBlocking 提供的接收器来调用 launch,使用 this.launch 或者简化的 launch。因此, launch 将成为 runBlocking 的子协程。正如父子关系那样,父协程的责任是等待所有的子协程,因此 runBlocking 将会挂起,直到所有的子协程完成:

fun main() = runBlocking {this.launch { // 和 launch 一样delay(1000L)println("World!")}launch {this.launch 一样delay(2000L)println("World!")}println("Hello,")
}
// Hello,
// (1 sec)
// World!
// (1 sec)
// World!

父协程为其子协程务提供一个作用域,它们都在该作用域中被调用。这构成了一种被称为结构化并发的关系。下面是父子关系的最重要影响:

  • 子协程从父协程继承上下文(但它们也可以重写它,这将在协程上下文一章中解释)
  • 父协程挂起,直到所有子协程都完成(这将在任务和子任务等待章节中解释)
  • 当父协程被取消时,其所有子协程也会被取消(这将在取消章节中解释)
  • 当子协程发生错误时,它也会销毁父协程(这将在异常章节中解释)

注意,与其他协程构建器不同,runBlocking 不是 CoroutineScope 上的扩展函数。这意味着它不能作为子协程:它只能用作根协程(层次结构中所有子协程的父协程)。这意味着 runBlocking 将不同于其他协程的情况。正如我们之前提到的,这与其它构建器非常不同。

更多示例

挂起函数需要从其他挂起函数调用。这一切都需要从一个协程构建器开始。除了 runBlocking,构建器需要在 CoroutineScope 上启动,在我们的简单示例中, runBlocking 提供了作用域,但在更大的应用程序中,作用域是由我们(我会在构建协程作用域章节中阐述)或者我们所使用的的框架(比如 Ktor 或 Android KTX 或 Android)来提供。一旦第一个构建器在一个作用域上启动,其他构建器就可以在第一个构建器的作用域上启动,依次类推,这就是我们应用程序的本质结构。

下面是一些在现实项目中如何使用协程的例子。前两个是后端和 Android 的经典用法。 MainPresenter 是Android 的典型用法。 UserController 则是后端的典型用法。

class NetworkUserRepository(private val api: UserApi,
) : UserRepository {suspend fun getUser(): User = api.getUser().toDomainUser()
}class NetworkNewsRepository(private val api: NewsApi,private val settings: SettingsRepository,
) : NewsRepository {suspend fun getNews(): List<News> = api.getNews().map { it.toDomainNews() }suspend fun getNewsSummary(): List<News> {val type = settings.getNewsSummaryType()return api.getNewsSummary(type)}
}class MainPresenter(private val view: MainView,private val userRepo: UserRepository,private val newsRepo: NewsRepository
) : BasePresenter {fun onCreate() {scope.launch {val user = userRepo.getUser()view.showUserData(user)}scope.launch {val news = async {newsRepo.getNews().sortedByDescending { it.date }}val newsSummary = async {newsRepo.getNewsSummary()}view.showNews(newsSummary.await(), news.await())}}
}@Controller
class UserController(private val tokenService: TokenService,private val userService: UserService,
) {@GetMapping("/me")suspend fun findUser(@PathVariable userId: String,@RequestHeader("Authorization") authorization: String): UserJson {val userId = tokenService.readUserId(authorization)val user = userService.findUserById(userId)return user.toJson()}
}

但是有一个问题:挂起函数该怎么办呢? 我们可以挂起,但是我们没有任何作用域。将作用域作为参数传递并不是一个好的解决方案(我们将在作用域函数一章中看到)。我们应该使用 coroutineScope 函数,它是一个挂起函数,为构建起创建一个协程。

使用 coroutineScope

假设在某个存储函数中,你需要异步加载两个资源,如用户数据和文章列表。在本例中,你希望只返回那些用户应该可以看到的文章。为了调用 async,我们需要一个作用域,但是我们不想把它传递给一个函数。要在挂起函数之外创建作用域,我们可以使用 coroutineScope 函数:

suspend fun getArticlesForUser(userToken: String?,
): List<ArticleJson> = coroutineScope {val articles = async { articleRepository.getArticles() }val user = userService.getUser(userToken)articles.await().filter { canSeeOnList(user, it) }.map { toArticleJson(it) }
}

coroutineScope 只是一个挂起函数,它为 lambda 表达式创建一个作用域。该函数返回 lambda 表达式,这个 lambda 可以返回任何内容(如 let、run、use、runBlocking)。在上面的例子中,lambda 表达式返回 List<ArticleJson>,所以 getArticlesForUser 也返回该类型。

coroutineScope 是在挂起函数内部需要作用域时使用的标准函数。这真的很重要,它的设计方法非常适合这个用例,但是要分析它,我们首先要学习一些关于上下文、取消和异常处理的知识,这就是为什么该函数将在后面的专门章节(协程作用域函数)中详细解释。

我们还可以开始将挂起的 main 函数与 coroutineScope 一起使用,这是 runBlocking 的主流替换方案:

suspend fun main(): Unit = coroutineScope {launch {delay(1000L)println("World!")}println("Hello,")
}
// Hello,
// (1 sec)
// World!

····图片····

这张图显示了 kotlinx.couroutines 库使用的不同元素。我们通常从作用域或者 runBlocking 开始。在这些函数中,我们可以调用其他构建器或挂起函数。我们不能在挂起函数上运行构建器,所以我们使用协程作用域函数(例如 coroutineScope

总结

这些知识对于使用大多数 Kotlin 协程来说已经足够了。在大多数情况下,我们只是使用挂起函数来调用其它挂起函数或普通函数。如果我们需要引入并发,我们可以使用 coroutineScope 包装函数,并在其作用域上使用构建器。我们将在后面的部分中学习如何构建这样的一个作用域,但对于大多数项目来说,它只需要定义一次,之后就很少涉及了。

虽然我们已经学会了基本知识,但是还有很多东西要学习。在下一章中,我们将深入探讨协程。我们将学习使用不同的上下文,如何处理取消、异常,如何测试协程,等等。还有很多很棒的功能等着我们去发现。

这篇关于深潜Kotlin协程(六):协程构建器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/430065

相关文章

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

maven 编译构建可以执行的jar包

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」👈,「stormsha的知识库」👈持续学习,不断总结,共同进步,为了踏实,做好当下事儿~ 专栏导航 Python系列: Python面试题合集,剑指大厂Git系列: Git操作技巧GO

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor

利用命令模式构建高效的手游后端架构

在现代手游开发中,后端架构的设计对于支持高并发、快速迭代和复杂游戏逻辑至关重要。命令模式作为一种行为设计模式,可以有效地解耦请求的发起者与接收者,提升系统的可维护性和扩展性。本文将深入探讨如何利用命令模式构建一个强大且灵活的手游后端架构。 1. 命令模式的概念与优势 命令模式通过将请求封装为对象,使得请求的发起者和接收者之间的耦合度降低。这种模式的主要优势包括: 解耦请求发起者与处理者

Jenkins构建Maven聚合工程,指定构建子模块

一、设置单独编译构建子模块 配置: 1、Root POM指向父pom.xml 2、Goals and options指定构建模块的参数: mvn -pl project1/project1-son -am clean package 单独构建project1-son项目以及它所依赖的其它项目。 说明: mvn clean package -pl 父级模块名/子模块名 -am参数

JAVA用最简单的方法来构建一个高可用的服务端,提升系统可用性

一、什么是提升系统的高可用性 JAVA服务端,顾名思义就是23体验网为用户提供服务的。停工时间,就是不能向用户提供服务的时间。高可用,就是系统具有高度可用性,尽量减少停工时间。如何用最简单的方法来搭建一个高效率可用的服务端JAVA呢? 停工的原因一般有: 服务器故障。例如服务器宕机,服务器网络出现问题,机房或者机架出现问题等;访问量急剧上升,导致服务器压力过大导致访问量急剧上升的原因;时间和

利用Django框架快速构建Web应用:从零到上线

随着互联网的发展,Web应用的需求日益增长,而Django作为一个高级的Python Web框架,以其强大的功能和灵活的架构,成为了众多开发者的选择。本文将指导你如何从零开始使用Django框架构建一个简单的Web应用,并将其部署到线上,让世界看到你的作品。 Django简介 Django是由Adrian Holovaty和Simon Willison于2005年开发的一个开源框架,旨在简

828华为云征文|华为云Flexus X实例docker部署rancher并构建k8s集群

828华为云征文|华为云Flexus X实例docker部署rancher并构建k8s集群 华为云最近正在举办828 B2B企业节,Flexus X实例的促销力度非常大,特别适合那些对算力性能有高要求的小伙伴。如果你有自建MySQL、Redis、Nginx等服务的需求,一定不要错过这个机会。赶紧去看看吧! 什么是华为云Flexus X实例 华为云Flexus X实例云服务是新一代开箱即用、体

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as