Kotlin协程启动缓慢

3

我一直在尝试对一个后端Kotlin应用程序进行性能评估,它只是拉取一些数据,进行一些数据转换并将其输出,没有什么特别的。其中一个引起我注意的事情是最后一步执行的部分,我们将最终数据倾泻到队列中。起初我注意到当我们启动应用程序时,最后一次网络调用需要很长时间,有时超过一秒钟。通常我们在协程中运行此网络调用,以防止最后一个调用阻塞一切,但我开始尝试单独计时协程和网络调用,并得到了一些奇怪的结果。从我看到的情况来看,与网络调用相比,协程可能需要花费很长时间才能启动/完成。我完全有可能没有正确记录事情,但这是我采用的一般计时方法:

val coroutineTime - Instant.now().toEpochMillis()
GlobalScope.launch {
executionTime = measureTimeMillis { <--DO Message Sending -->}
totalTime = Instant.now().toEpochMillis() - coroutineTime 
// Log out execution Time and total time 
}

现在我将看到的是类似于

- totalTime = ~800ms 
- executionTime = ~150ms 

这些并不是一次性的操作,我同时进行了多个这样的进程(我想最多有10个线程),而第一个总时间将始终比实际执行时间/网络调用时间要长得多。最终,在新的几十条消息之后,开销会减少,这些时间将在大约15ms左右变得相同,但是在协程启动时几乎有700ms的开销对我来说似乎是疯狂的。

这是正常/预期的行为吗?我在另一个应用程序中测试过,并看到类似但不那么极端的结果,其中第一个协程需要约70ms才能启动,我很难在使用kotlin的android开发之外找到任何其他此类讨论的例子。

3个回答

1
作为第一条注意事项,除非您真的知道自己在做什么,否则几乎从来不建议使用GlobalScope。这就是为什么它被标记为敏感API。相反,您应该使用适当关闭的范围(遵循启动此工作的任何组件的生命周期)。
现在,据我所知,这个GlobalScope在默认调度程序上运行,因此可能是由于默认线程池的冷启动引起的。稍后,根据您拥有的并发协程数量,使用此调度程序进行网络调用也可能会出现问题。更适合使用Disptachers.IO来处理I/O绑定的工作(或自定义线程池)。
这仍然无法解释冷启动,但在调查之前,我首先会更改它。

1
我觉得这实际上解释了“冷启动”现象。楼主说有多个类似的进程同时被调用。如果它们的数量大于CPU核心的数量,那么线程池中的所有线程都将被阻塞,一些协程将不得不等待被调度。 - undefined
没错,如果OP在谈论的是在nCores个协程之后启动的协程,那么这可以解释为什么冷启动会慢。但是,nCores个协程应该启动得很快,我以为OP是说即使是最开始的那些协程也很慢。 - undefined
谢谢大家的回复,伙计们。我再试了一次,使用Dispatchers.IO来进行双重检查,但结果还是一样,网络调用和协程调用之间存在很大的差异。我唯一能想到的是,在网络调用之前可能有一些相当繁重的处理工作正在进行,而这些工作可能在其他10个线程中进行(它们是由JMSListener管理的线程,所以线程数量根据负载而变化),可能会长时间占用CPU资源。我打算减少线程数量再试一次,因为除此之外,Dispatchers.IO似乎没有产生积极的效果。 - undefined

0

我遇到了类似的问题,我有一个函数从共享首选项加载一些数据,对数据进行一些计算(所有这些都在Dispatcher.Default中完成),并在Dispatcher.Main上返回结果。我测量协程在实际开始执行Dispatchers.Main.launch {}内部块之后(在tag2到tag3之间的时间)花费了多长时间,结果大约是950毫秒(!)以下是该函数:

fun someName() {
    CoroutineScope(Dispatchers.Default).launch {
        val time = System.currentTimeMillis()
        //load data and calculations
        Log.d("tag2", "load and calculations took " + (System.currentTimeMillis() - time))
        CoroutineScope(Dispatchers.Main.immediate).launch {
            Log.d("tag3", "reached main thread code " + (System.currentTimeMillis() - time))
            //do something
            Log.d("tag4", "do something took " + (System.currentTimeMillis() - time))
        }
    }
}

但后来我意识到这是在应用程序启动时发生的,主线程正在忙于创建所有UI,因此即使使用.immediate,也需要一些时间才能让主线程执行分派的代码...然后我尝试在应用程序已经启动并等待后运行此函数,并发现从tag2到tag3只需要大约1毫秒(使用.immediate)。因此看起来当在协程上调度某些内容时,当线程不忙时,它将立即开始。

-1

如果您不恰当地使用协程,那么这是预期的行为 ;-)

我猜测您的消息发送是一个阻塞操作。默认情况下,GlobalScope.launch()通过 Dispatchers.Default 调度协程,该调度器专门用于执行 CPU 密集型操作,它具有有限数量的线程,使用时不能阻塞。 如果您这样做,可能会耗尽线程资源,导致协程需要等待一些阻塞操作完成。

如果您需要运行阻塞或 IO 代码,则应改用 Dispatchers.IO

GlobalScope.launch(Dispatchers.IO) {

抱歉,我应该提到,我尝试了几种不同的方法,包括使用GlobalScope(Dispatchers.IO)CoroutineScope(Dispatchers.IO),我确信我看到了相同的情况。我会再试一次,争取在有机会时得到一些实际的数据。关于你对Joffrey的回答的背景,我正在使用一个最多可以扩展到10个线程的JMSListener,如果我在具有2个CPU单元的kubes上运行,我可以假设我有2个核心,那么这些协程是否可能与线程创建竞争CPU资源? - undefined
我不确定你的具体架构是什么,以及你所说的有10个线程是什么意思。但即使你只使用一个线程,并且用它调用你上面的代码3次,那么前两个操作将在IO上阻塞,第三个操作将等待它们完成。然后,第三次调用的totalTimeexecutionTime之间会出现很大的差异。 - undefined
GlobalScope 在 Android 中推荐使用吗? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接