为什么我放弃使用kotlin中的协程?(代码片段)

author author     2022-12-12     534

关键词:

为什么我放弃使用 Kotlin 中的协程?

实不相瞒,我对 Kotlin 这门编程语言非常喜欢,尽管它有一些缺点和奇怪的设计选择。我曾经参与过一个使用 Kotlin、Kotlin 协程(coroutine, 下同)和基于协程的服务器框架 KTOR 的中型项目。这个技术组合提供了很多优点,但是我也发现,与常规的 Spring Boot 相比,它们很难使用。

声明:我无意抨击相关技术,我的目的仅是分享我的使用体验,并解释为什么我以后不再考虑使用。

调试

请看下面一段代码。

suspend fun retrieveData(): SomeData 
    val request = createRequest()
    val response = remoteCall(request)
    return postProcess(response)


private suspend fun remoteCall(request: Request): Response 
   // do suspending REST call

假设我们要调试 retrieveData 函数,可以在第一行中放置一个断点。然后启动调试器(我使用的是 IntelliJ),它在断点处停止。现在我们执行一个 Step Over(跳过调用 createRequest),这也正常。但是如果再次 Step Over,程序就会直接运行,调用 remoteCall() 之后不会停止。

为什么会这样?JVM 调试器被绑定到一个 Thread 对象上。当然,这是一个非常合理的选择。然而,当引入协程之后,一个线程不再做一件事。仔细一看:remoteCall(request) 调用的是一个 suspend 函数,虽然我们在调用它的时候并没有在语法中看到它。那么会发生什么?我们执行调试器 "step over ",调试器运行 remoteCall 的代码并等待。

这就是难点所在:当前线程(我们的调试器被绑定到该线程)只是我们的coroutine 的执行者。当我们调用 suspend 函数时,会发生的情况是,在某个时刻,suspend 函数会 yield。这意味着另外一个 Thread 将继续执行我们的方法。我们有效地欺骗了调试器。

我发现的唯一的解决方法是在我想执行的行上放置一个断点,而不是使用Step Over。不用说,这是个大麻烦。而且很显然,这不仅仅是我一个人的问题。

此外,在一般的调试中,很难确定一个单一的 coroutine 当前在做什么,因为它在线程之间跳跃。当然,coroutine 是有名字的,你可以在日志中不仅打印线程,还可以打印 coroutine 的名字,但根据我的经验,调试基于 coroutine 的代码所需的心智负担,要比基于线程的代码高很多。

REST 调用中绑定 context 数据

在微服务上开发,一个常见的设计模式是,接收一个某种形式认证的 REST 调用,并将相同的认证传递给其他微服务的所有内部调用。在最简单的情况下,我们至少要保留调用者的用户名。

然而,如果这些对其他微服务的调用在我们调用栈中嵌套了 10 层深度怎么办?我们当然不希望在每个函数中都传递一个认证对象作为参数。我们需要某种形式的 "context",这种 context 是隐性存在的。

在传统的基于线程的框架中,如 Spring,解决这个问题的方法是使用 ThreadLocal 对象。这使得我们可以将任何一种数据绑定到当前线程。只要一个线程对应一个 REST 调用(你应该始终以这个为目标),这正是我们需要的。这个模式的很好的例子是 Spring 的 SecurityContextHolder。

对于 coroutine,情况就不同了。一个 ThreadLocal 不再对应一个协程,因为你的工作负载会从一个线程跳到另一个线程;不再是一个线程在其整个生命周期内伴随一个请求。在 Kotlin coroutine 中,有 CoroutineContext。本质上,它不过是一个 HashMap,与 coroutine 一起携带(无论它运行在哪个线程上)。它有一个可怕的过度设计的 API,使用起来很麻烦,但这不是这里的主要问题。

真正的问题是,coroutine 不会自动继承上下文。

例如:

suspend fun sum(): Int 
    val jobs = mutableListOf<Deferred<Int>>()
    for(child in children)
        jobs += async   // we lose our context here!
            child.evaluate() 
        
    
    return jobs.awaitAll().sum()

每当你调用一个 coroutine builder,如 async、runBlocking 或 launch,你将(默认情况下)失去你当前的 coroutine 上下文。你可以通过将上下文显式地传递到 builder 方法中来避免这种情况,但是上帝保佑你不要忘记这样做(编译器不会管这些!)。

一个子 coroutine 可以从一个空的上下文开始,如果有一个上下文元素的请求进来,但没有找到任何东西,可以向父 coroutine 上下文请求该元素。然而,在 Kotlin 中不会发生这种情况,开发人员需要手动完成,每一次都是如此。

如果你对这个问题的细节感兴趣,我建议你看看这篇博文。

https://blog.tpersson.io/2018/04/22/emulating-request-scoped-objects-with-kotlin-coroutines/

synchronized 不再如你想的那样工作

在 Java 中处理锁或 synchronized 同步块时,我考虑的语义通常是 "当我在这个块中执行时,其他调用不能进入"。当然“其他调用”意味着存在某种身份,在这里就是线程,这应该在你的脑海中升起一个大红色的警告信号。

看看下面的例子。

val lock = ReentrantLock()

suspend fun doWithLock()
   lock.withLock 
       callSuspendingFunction()
   

这个调用很危险,即使 callSuspendingFunction() 没有做任何危险的操作,代码也不会像你想象的那样工作。

  • 进入同步锁
  • 调用 suspend 功能
  • 协程 yield,当前线程仍然持有锁。
  • 另一个线程继续我们的 coroutine
  • 还是同一个协程,但我们不再是锁的 owner 了!

潜在的冲突、死锁或其他不安全的情况数量惊人。你可能会说,我们只是需要设计我们的代码来处理这个问题。我同意,然而我们谈论的是 JVM。那里有一个庞大的 Java 库生态。而它们并没有做好处理这些情况的准备。

这里的结果是:当你开始使用 coroutine 的时候,你就放弃了使用很多 Java 库的可能性,因为它们目前只能工作在基于线程的环境。

单机吞吐量与水平扩展

对于服务器端来说,coroutine 的一大优势是,一个线程可以处理更多的请求;当一个请求等待数据库响应时,同一个线程可以愉快地服务另一个请求。特别是对于 I/O 密集型任务,这可以提高吞吐量。

然而,正如这篇博文所希望向您展示的那样,在许多层面上,使用 coroutine 都有一个非零成本的开销。

由此产生的问题是:这个收益是否值得这个成本?而在我看来,答案是否定的。在云和微服务环境中,有一些现成的扩展机制,无论是 Google AppEngine、AWS Beanstalk 还是某种形式的 Kubernetes。如果当前负载增加,这些技术将简单地按需生成你的微服务的新实例。因此,考虑到引入 coroutine 带来的额外成本,单一实例所能处理的吞吐量就不那么重要了。这就降低了我们使用 coroutine 所获得的价值。

Coroutine 有其存在的价值

话说回来,Coroutine 还是有其使用场景。当开发只有一个 UI 线程的客户端 UI 时,coroutine 可以帮助改善你的代码结构,同时符合 UI 框架的要求。听说这个在安卓系统上很好用。Coroutine 是一个有趣的主题,然而对于服务器端开发来说,我觉得协程还差点意思。JVM 开发团队目前正在开发 Fiber,本质上也是 coroutine,但他们的目标是与 JVM 基础库更好共存。这将是有趣的,看它将来如何发展,以及 Jetbrains 对 Kotlin coroutine 对此会有什么反应。在最好的情况下,Kotlin coroutine 将来只是简单映射到 Fiber 上,而调试器也能足够聪明来正确处理它们。

英文原文:
https://dev.to/martinhaeusler/why-i-stopped-using-coroutines-in-kotlin-kg0

参考阅读:

  • 基准测试表明, Async Python 远不如同步方式
  • 谈谈PHP8新特性Attributes
  • 如何做好Code Review? 分享一份我们团队的 Checklist
  • 分布式算法 Paxos 的直观解释 (TL;DR)
  • 重构,还是重写?(2020版)

本文由高可用架构翻译,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构

改变互联网的构建方式

技术图片

译文kotlin1.3版本的协程(代码片段)

...oleiva.com/coroutines/协程是kotlin中让人激动的特性之一,使用协程,可以用一种优雅的方式来简化异步编程,让代码更加可读和易于理解。使用协程,你可以用同步的方式写异步代码,而不是传统的Callback方式来... 查看详情

kotlin协程协程取消③(finally释放协程资源|使用use函数执行closeable对象释放资源操作|构造无法取消的协程任务|构造超时取消的协程任务)(代码片段)

文章目录一、释放协程资源二、使用use函数执行Closeable对象释放资源操作三、使用withContext(NonCancellable)构造无法取消的协程任务四、使用withTimeoutOrNull函数构造超时取消的协程任务一、释放协程资源如果协程中途取消,期间需要释... 查看详情

kotlin协程协程取消③(finally释放协程资源|使用use函数执行closeable对象释放资源操作|构造无法取消的协程任务|构造超时取消的协程任务)(代码片段)

文章目录一、释放协程资源二、使用use函数执行Closeable对象释放资源操作三、使用withContext(NonCancellable)构造无法取消的协程任务四、使用withTimeoutOrNull函数构造超时取消的协程任务一、释放协程资源如果协程中途取消,期间需要释... 查看详情

译文kotlin1.3版本的协程(代码片段)

...f;我们马上即可学习它,在此之前,让我们了解下为什么协程很有必要。协程是kotlin1.1推出的实验特性,在kotlin1.3版本发布了最终的Api,现在已经可以投入生产。Coroutinesgoal:Theproblem假设你需要开发一个登录页面... 查看详情

快速上手kotlin开发系列之什么是协程(代码片段)

...巨人的肩膀上做个笔记,摘录自:https://kaixue.io/kotlin-coroutines-1协程是什么协程的概念并没有官方的或者统一的定义,协程原本是一个跟线程非常类似的用于处理多任务的概念,是一种编程思想,并不局限于特... 查看详情

day10:kotlin的协程已经安卓网络技术初步(代码片段)

...析网络上常见的两种数据1.xml格式2.json格式三、Retrofit的使用四:Kotlin:协程一、利用Http访问网络GET代表希望从服务器那里获取数据POST则代表向服务器提交数据网络请求一般在子线程中执行,不然可能会阻塞主线程导... 查看详情

使用context关闭协程以及协程中的协程(代码片段)

packagemainimport("sync""context""fmt""time")varwgsync.WaitGroupfuncworker2(ctxcontext.Context)LOOP:forfmt.Printf("worker2")time.Sleep(time.Second)selectcase<-ctx.Done():breakLOOPdefault:fu 查看详情

kotlin学习手记——协程进阶(代码片段)

作用域:顶级:coroutineScope表示协同作用域,coroutineScope内部的协程出现异常可以挂掉外部协程,会向外部传播,外部协程挂掉也会挂掉子协程,即双向传播。supervisorScope表示主从作用域,supervisorScope内... 查看详情

tornado中的协程是如何工作的(代码片段)

协程定义Coroutinesarecomputerprogramcomponentsthatgeneralizesubroutinesfornonpreemptivemultitasking,byallowingmultipleentrypointsforsuspendingandresumingexecutionatcertainlocations.。――[维基百科]我们在平常编程中,更习惯使用 查看详情

android上的协程(第一部分):了解背景(代码片段)

...1f;Kotlin协程引入了一种新的并发方式,可以在Android上使用它来简化异步代码。虽然协程在1.3中是Kotlin的新概念,但协程的概念自编程语言诞生以来就一直存在。第一个探索使用协程的语言是1967年的Simula。在过去的几年中... 查看详情

kotlin--知识点协程(代码片段)

...函数?三、非阻塞式挂起1.什么是「非阻塞式挂起」2.为什么要讲非阻塞式挂起3.协程与线程一、什么是协程1.介绍协程并不是Kotlin提出来的新概念,其他的一些编程语言,例如:Go、Python等都可以在语言层面上实现... 查看详情

kotlin协程flow异步流⑥(调用flow#launchin函数指定流收集协程|通过取消流收集所在的协程取消流)(代码片段)

文章目录一、调用Flow#launchIn函数指定流收集协程1、指定流收集协程2、Flow#launchIn函数原型3、代码示例二、通过取消流收集所在的协程取消流一、调用Flow#launchIn函数指定流收集协程1、指定流收集协程响应式编程,是基于事件驱动... 查看详情

kotlin协程flow异步流⑥(调用flow#launchin函数指定流收集协程|通过取消流收集所在的协程取消流)(代码片段)

文章目录一、调用Flow#launchIn函数指定流收集协程1、指定流收集协程2、Flow#launchIn函数原型3、代码示例二、通过取消流收集所在的协程取消流一、调用Flow#launchIn函数指定流收集协程1、指定流收集协程响应式编程,是基于事件驱动... 查看详情

python之协程(代码片段)

...协程问题,Python的协程相对原理上来说要简单很多了。在使用Java做开发的时候,经常使用线程,过程中经常也有听说过微线程/协程的概念,但是没有去深刻的学习过Java的协程是怎么实现的。这一篇文章主要是讲述Python中的协程... 查看详情

unity中的协程(代码片段)

 //ThecoroutinewillcontinueafterallUpdatefunctionshavebeencalledonthenextframe.yieldreturn1;//Continueafteraspecifiedtimedelay,afterallUpdatefunctionshavebeencalledfortheframe.yieldreturnnewWaitF 查看详情

kotlin协程(代码片段)

一、CoroutineScope协程作用域,通过socpe启动一个或者多个协程,同一个socpe启动都算他的子协程GlobalScope//全局协程,生命周期与app相同MainScope()//在主线程启动协程viewmodelScope//生命周期与viewmodel绑定的协程privatevalmainScop... 查看详情

谈谈我对kotlin中协程的理解(代码片段)

文章目录1协程(Coroutines)是什么2你需要用协程吗?3使用协程优点4kotlin协程的演进5实现方式5.1环境准备5.2创建协程的几种方式6协程的应用场景6.1从相册中读取图片并显示6.2AndroidJetpack中使用kotlin协程6.2.1在ViewModel中使用ViewModel... 查看详情

unity中的协程用法以及注意事项(代码片段)

...章节,将简单的总结一下如何开启协程,关闭协程,以及使用协程的注意事项。   一、如何开启协程:privatevoidStart()m_SpherePrefab=Resources.Load<GameObject>("Test/Sphere_00");Debug.Log("m_SpherePrefab="+m_SpherePrefab);#region协程的学习及 查看详情