老实说,我真的很喜欢Kotlin作为一种编程语言,尽管它有一些缺点和奇怪的设计选择。我参与过一个中型项目使用Kotlin,Kotlin协程(coroutine,下同)和基于协程的服务器框架KTOR。这种技术组合提供了很多优势,但我也发现与常规SpringBoot相比它们很难使用。免责声明:我无意批评相关技术,我的目的只是分享我的经验并解释为什么我以后不会考虑使用它。调试请看下面的一段代码。suspendfunretrieveData():SomeData{valrequest=createRequest()valresponse=remoteCall(request)returnpostProcess(response)}privatesuspendfunremoteCall(request:Request):Response{//dosuspendingRESTcall}假设我们要调试retrieveData函数,我们可以下断点。然后启动调试器(我正在使用IntelliJ)并在断点处停止。现在我们做一个StepOver(跳过调用createRequest),这也是正常的。但是如果再跨步过去,程序会直接运行,调用remoteCall()后不会停止。为什么会这样?JVM调试器绑定到Thread对象。当然,这是一个非常合理的选择。但是,引入协程后,一个线程不再只做一件事。仔细看看:remoteCall(request)调用了一个挂起函数,虽然我们在调用它时并没有在语法中看到它。那么会发生什么?我们执行调试器“stepover”,调试器运行remoteCall的代码并等待。这是棘手的部分:当前线程(我们的调试器绑定到的线程)只是我们协程的执行者。当我们调用挂起函数时会发生什么,在某个时候,挂起函数会产生。这意味着另一个线程将继续执行我们的方法。我们有效地欺骗了调试器。我发现的唯一解决方法是在我要执行的行上放置一个断点,而不是使用StepOver。不用说,这是一团糟。显然,这不仅仅是我的问题。此外,在一般调试中,很难确定单个协程在线程之间跳转时当前正在做什么。当然,协程是有名字的,你不仅可以打印线程,还可以在日志中打印协程的名字,但以我的经验,调试基于协程的代码所需的精神负担远高于基于线程的代码.在REST调用中绑定上下文数据在微服务上开发,一种常见的设计模式是接收具有某种形式身份验证的REST调用,并将相同的身份验证传递给其他微服务的所有内部调用。在最简单的情况下,我们希望至少保留调用者的用户名。但是,如果这些对其他微服务的调用嵌套在我们的调用堆栈中的10层深处会怎样?我们当然不想在每个函数中都将身份验证对象作为参数传递。我们需要某种形式的“上下文”,它隐含地存在。在传统的基于线程的框架中,比如Spring,解决这个问题的方法是使用ThreadLocal对象。这允许我们将任何类型的数据绑定到当前线程。只要一个线程对应一个REST调用(您应该始终将其作为目标),这正是我们所需要的。这种模式的一个很好的例子是Spring的SecurityContextHolder。对于协程,情况有所不同。一个ThreadLocal不再对应一个协程,因为你的工作负载会从一个线程跳转到另一个线程;不再是整个生命周期都有请求的线程。在Kotlin协程中,有CoroutineContext。本质上,它只不过是一个HashMap,与协程一起携带(无论它在哪个线程上运行)。它有一个可怕的过度设计的API,使用起来很麻烦,但这不是这里的主要问题。真正的问题是协程不会自动继承上下文。例如:suspendfunsum():Int{valjobs=mutableListOf>()for(childinchildren){jobs+=async{//weloseourcontexthere!child.evaluate()}}returnjobs.awaitAll().sum()}per当您调用协程构建器(如async、runBlocking或launch)时,您将(默认情况下)丢失当前的协程上下文。您可以通过显式将上下文传递给构建器方法来避免这种情况,但上帝保佑您不要忘记这样做(编译器不关心!)。子协程可以从空上下文开始,如果请求上下文元素,但没有找到任何内容,它可以从父协程上下文请求该元素。然而,在Kotlin中,这不会发生,开发人员每次都需要手动完成。如果您对这个问题的详细信息感兴趣,我建议您看看这篇博文。https://blog.tpersson.io/2018/04/22/emulating-request-scoped-objects-with-kotlin-coroutines/synchronized不再像你想象的那样工作在处理Java中的锁或同步同步块时,语义我想到的通常是“当我在这个块中执行时,没有其他调用可以进来”。当然,“其他电话”意味着有某种身份,在这种情况下是线程,它应该在您的脑海中发出一个红色的大警告标志。看看下面的例子。vallock=ReentrantLock()suspendfundoWithLock(){lock.withLock{callSuspendingFunction()}}这个调用是危险的,即使callSuspendingFunction()没有做任何危险的事情,代码也不会像你想的那样工作。进入同步锁调用suspend函数coroutineyield,当前线程仍持有锁。另一个继续我们协程的线程仍然是同一个协程,但我们不再是锁的所有者!潜在冲突、死锁或其他不安全情况的数量惊人。你可能会说,我们只需要设计我们的代码来处理这个。我同意,但是我们正在谈论JVM。那里有一个巨大的Java库生态系统。他们不准备处理这些情况。这里的结果是:当你开始使用协程时,你就放弃了使用许多Java库的可能性,因为它们目前只能在基于线程的环境中工作。单机吞吐量和横向扩展对于服务器端来说,协程的一大优势是一个线程可以处理更多的请求;当一个请求正在等待数据库响应时,同一个线程可以愉快地服务于另一个请求。特别是对于I/O密集型任务,这可以提高吞吐量。然而,正如这篇博文希望向您展示的那样,使用协程在许多层面上都具有非零成本开销。出现的问题是:收益是否值得付出代价?在我看来,答案是否定的。在云和微服务环境中,有一些开箱即用的扩展机制,无论是GoogleAppEngine、AWSBeanstalk还是某种形式的Kubernetes。如果当前负载增加,这些技术将简单地按需生成微服务的新实例。因此,考虑到引入协程的额外成本,单个实例可以处理的吞吐量就不那么重要了。这降低了我们从协程中获得的价值。协程有它的价值。话虽如此,Coroutine还是有它的使用场景的。在开发只有一个UI线程的客户端UI时,协程可以帮助您改进代码结构,同时符合UI框架要求。听说这在Android上效果很好。协程是一个有趣的话题,但是对于服务器端开发,我认为协程没有那么有趣。JVM开发团队目前正在开发Fiber,它本质上是一个协程,但他们的目标是更好地与JVM基础库共存。看看它在未来如何发展以及Jetbrains如何通过Kotlin协程对其作出反应将会很有趣。在最好的情况下,Kotlin协程将在未来简单地映射到Fiber,并且调试器将足够智能以正确处理它们。英文原文:https://dev.to/martinhaeusler/why-i-stopped-using-coroutines-in-kotlin-kg0本文转载自微信公众号《高可用架构》,可通过关注以下二维码。转载本文请联系高可用架构公众号。