为什么线程表现出比协同程序更好的性能?

问题描述 投票:9回答:2

我编写了3个简单的程序来测试协程的线程性能优势。每个程序都进行了许多常见的简单计算。所有程序都是彼此分开运行的。除了执行时间,我通过Visual VM IDE插件测量CPU使用率。

  1. 第一个程序使用1000-threaded池进行所有计算。由于频繁的上下文变化,这段代码显示了与其他结果相比最差的结果(64326 ms): val executor = Executors.newFixedThreadPool(1000) time = generateSequence { measureTimeMillis { val comps = mutableListOf<Future<Int>>() for (i in 1..1_000_000) { comps += executor.submit<Int> { computation2(); 15 } } comps.map { it.get() }.sum() } }.take(100).sum() println("Completed in $time ms") executor.shutdownNow()

first program

  1. 第二个程序具有相同的逻辑,但它不使用1000-threaded池,而是仅使用n-threaded池(其中n等于机器核心的数量)。它显示了更好的结果(43939 ms)并且使用更少的线程也是好的。 val executor2 = Executors.newFixedThreadPool(4) time = generateSequence { measureTimeMillis { val comps = mutableListOf<Future<Int>>() for (i in 1..1_000_000) { comps += executor2.submit<Int> { computation2(); 15 } } comps.map { it.get() }.sum() } }.take(100).sum() println("Completed in $time ms") executor2.shutdownNow()

second program

  1. 第三个程序用协同程序编写,结果显示差异很大(从41784 ms81101 ms)。我很困惑,并且不太明白为什么它们如此不同以及为什么协同程序有时比线程慢(考虑到小异步计算是协同程序的强项)。这是代码: time = generateSequence { runBlocking { measureTimeMillis { val comps = mutableListOf<Deferred<Int>>() for (i in 1..1_000_000) { comps += async { computation2(); 15 } } comps.map { it.await() }.sum() } } }.take(100).sum() println("Completed in $time ms")

third program

我实际上已经阅读了很多关于这些协同程序以及它们如何在kotlin中实现的内容,但实际上我并没有看到它们按预期工作。我做基准错误了吗?或者我可能使用coroutines错了?

kotlin benchmarking kotlin-coroutines
2个回答
15
投票

你设置问题的方式,你不应该期望从协同程序中获得任何好处。在所有情况下,您都会向执行者提交一个不可分割的计算块。你没有利用协程暂停的概念,在那里你可以编写实际上被切断并分段执行的顺序代码,可能在不同的线程上。

大多数协同程序的使用案例都围绕阻塞代码:避免你让线程不做任何事情而是等待响应的情况。它们也可能用于交错CPU密集型任务,但这是一种更加特殊的方案。

我建议对涉及多个顺序阻塞步骤的1,000,000个任务进行基准测试,例如在Roman Elizarov's KotlinConf 2017 talk中:

suspend fun postItem(item: Item) {
    val token = requestToken()
    val post = createPost(token, item)
    processPost(post)
}

所有requestToken()createPost()processPost()都涉及网络电话。

如果您有两个这样的实现,一个使用suspend funs,另一个使用常规阻塞函数,例如:

fun requestToken() {
   Thread.sleep(1000)
   return "token"
}

suspend fun requestToken() {
    delay(1000)
    return "token"
}

你会发现你甚至无法设置执行第一个版本的1,000,000个并发调用,如果你将数字降低到没有OutOfMemoryException: unable to create new native thread你实际可以实现的数量,协同程序的性能优势应该是显而易见的。

如果您想探索协同程序对CPU绑定任务的可能优势,您需要一个用例,无论您是顺序执行还是并行执行它们都无关紧要。在上面的示例中,这被视为一个不相关的内部细节:在一个版本中,您运行1,000个并发任务,而在另一个版本中,您只使用四个,因此它几乎是顺序执行。

Hazelcast Jet是这种用例的一个例子,因为计算任务是相互依赖的:一个输出是另一个输入。在这种情况下,您不能只运行其中的一些直到完成,在一个小线程池上,您实际上必须交错它们,以便缓冲输出不会爆炸。如果您尝试使用和不使用协同程序设置这样的场景,您将再次发现您要么分配与任务一样多的线程,要么使用可挂起的协程,后一种方法获胜。 Hazelcast Jet在普通Java API中实现了协同程序的精神。这在reference manual中讨论过。它的方法将从协程编程模型中获益,但目前它是纯Java。

披露:本文的作者属于Jet工程团队。


6
投票

协程不是设计得比线程更快,它是为了降低RAM消耗和更好的异步调用语法。


0
投票

协同程序设计为轻量级线程。它使用较低的RAM,因为当您执行1,000,000个并发例程时,它不必创建1,000,000个线程。 Coroutine可以帮助您优化线程使用,并提高执行效率,您不再需要关心线程。您可以将协同程序视为可运行或任务,您可以将其发布到处理程序中并在线程中执行。

© www.soinside.com 2019 - 2024. All rights reserved.