首页 > 解决方案 > 使用协程实现后应用程序变慢

问题描述

我已将所有数据库表 greenDao 迁移到 Room 并带有挂起功能应用程序速度和性能在 CoroutineScope 调用函数时变慢。

withContext(Dispatchers.IO) 在 coroutineScope(Dispatcher.Main) 中运行非常慢,而 runBlocking 在没有协程的情况下运行很快

// This function running fast

    fun getValFromDb() {
      
            var objectOne: MyVal1 = runBlocking{ fetchFromTabelOne() }
            var objectTwo: MyVal2 = runBlocking{ fetchFromTableTwoById(objectOne.getId) }
            UpdateUi(objectTwo) 
        
    }



// This function running slow 
fun getValFromDb() {
   
      CoroutineScope(Dispatchers.Main).launch{ 
      
        var objectOne: MyVal = withContext(Dispatchers.IO){ fetchFromDb() }
        
        var objectTwo: MyVal2 = withContext(Dispatchers.IO){ fetchFromTableTwoById(objectOne.getId) }
        
        UpdateUi(objectTwo) 
    }
}

注意:我们没有找到任何解决方案或建议在生产环境中实施 runBlocking,但与 runBlocking 相比,CoroutineScope 一直在减慢应用程序的速度

标签: kotlinkotlin-coroutinescoroutine

解决方案


编辑:这个答案假设您控制这里的大部分代码。评论中提供的代码堆链接显示有问题的函数是一个覆盖,父类可能不受 OP 的控制。如果此方法希望您Cursor同步使用数据,则最好坚持使用runBlocking(除非在主线程上调用它)。


原始答案

新代码有 3 个主要问题:

  1. withContext(Dispatchers.IO)强制切换到另一个线程池(因为协程的当前调度程序是Main),所以它会减慢一点速度。此外,Room 在后台使用自己的调度程序来实现其挂起功能,因此它也会在那里切换上下文。这withContext(IO)不仅是多余的,而且是有害的,因为它强制至少 2 次上下文切换。

  2. launch不等价于runBlocking,因为执行变成异步的。runBlocking仅当其主体内的所有内容都完成时才返回,包括启动的协程等。但是,现在您使用launch,对 的调用launch几乎会立即返回,并且您无法保证继续前进时主体会完成。这是否可行实际上取决于您的函数的调用者,更具体地说,调用者在调用后做了什么(它是否依赖于正在完成的操作?)。这会极大地影响您的应用程序的行为,而不仅仅是它的性能(并且性能下降可能是行为变化的副作用)。

  3. CoroutineScope()是一个创建新协程作用域的工厂函数。创建自定义范围是可以的,但应该小心。协程作用域的重点是控制在其中启动的协程的生命周期。通常建议在具有生命周期的组件中创建协程作用域,并在组件关闭/销毁/处置时取消该作用域。当您在 OP 的代码中创建一个范围时,您不会保留该范围的句柄,并且cancel()在适当的时候不能处理,从而有效地泄漏协程。这就是结构化并发应该防止的。

问题 #1 的解决方案就是移除withContext包装器。你不应该需要它们。

对于问题 #2,我无法真正回答这个问题,因为它取决于上下文,而且这里没有足够的信息。

对于问题 #3,您有多种选择。您可以做的一些示例:

  • 制作你的函数suspend,而不是创建一个范围和launch-ing 一个协程。这意味着您将创建协程的责任移到调用堆栈上,允许调用者选择范围。如果这样做,请确保将 UI 更新包装在其中,withContext(Dispatchers.Main)以便它在主线程中运行,而不管调用者的当前上下文如何

  • 通过将您的函数定义为扩展CoroutineScope(或将其作为参数),使调用者将协程范围传递给您的函数。再次确保您在主线程中更新您的 UI,就像在前一点一样(因为您无法知道此时您将在哪个调度程序中运行)。这可以通过将调度程序传递给launch或通过使用withContextUI 更新来完成,就像前一点一样。

  • 创建一个自定义范围作为运行此代码的类的属性,但在这种情况下,您需要确保您的类具有某种生命周期,并且当您的类生命周期结束时该范围被取消。


推荐阅读