为什么线程显示比协程更好的性能?

我写了3个简单的程序来测试协程在线程上的性能优势。 每个程序都做了很多简单的计算。 所有的程序都是分开运行的。 除了执行时间外,我还通过Visual VM IDE插件测量了CPU的使用情况。

  1. 第一个程序使用1000-threaded池进行所有计算。 由于频繁的上下文更改, 64326 ms代码显示了最差的结果( 64326 ms

     val executor = Executors.newFixedThreadPool(1000) time = generateSequence { measureTimeMillis { val comps = mutableListOf<Future>() for (i in 0..1_000_000) { comps += executor.submit { computation2(); 15 } } comps.map { it.get() }.sum() } }.take(100).sum() println("Completed in $time ms") executor.shutdownNow() 

第一个程序

  1. 第二个程序具有相同的逻辑,而不是1000-threaded池,它只使用n-threaded池(其中n等于机器内核的数量)。 它显示更好的结果( 43939 ms ),并使用较少的线程,这也是好的。

     val executor2 = Executors.newFixedThreadPool(4) time = generateSequence { measureTimeMillis { val comps = mutableListOf<Future>() for (i in 0..1_000_000) { comps += executor2.submit { computation2(); 15 } } comps.map { it.get() }.sum() } }.take(100).sum() println("Completed in $time ms") executor2.shutdownNow() 

第二个程序

  1. 第三个程序是用协程编写的,结果显示差异很大(从41784 ms81101 ms )。 我很困惑,不太明白为什么它们如此不同以及为什么协程有时比线程慢(考虑到小的异步计算是协程的一个重要部分)。 这里是代码:

     time = generateSequence { runBlocking { measureTimeMillis { val comps = mutableListOf<Deferred>() for (i in 0..1_000_000) { comps += async { computation2(); 15 } } comps.map { it.await() }.sum() } } }.take(100).sum() println("Completed in $time ms") 

第三方案

我实际上阅读了很多关于这些协同程序的知识,以及它们是如何在kotlin中实现的,但是实际上我并没有看到它们按预期工作。 我在做我的基准测试错误吗? 或者,也许我使用协程错误?

你设置你的问题的方式,你不应该期望从协程的任何好处。 在所有情况下,您都要向执行者提交一个不可分割的计算块。 你并没有利用协程暂停的想法,在那里你可以编写连续的代码,实际上可以在不同的线程上分段执行。

协程的大多数用例都围绕着阻塞代码:避免一个线程只能等待响应的情况。 它们也可能被用来交错CPU密集型任务,但这是一个更特殊的情况。

我会建议对涉及几个连续阻塞步骤的1,000,000个任务进行基准测试,例如Roman Elizarov的2017年KotlinConf会议 :

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

所有requestToken()createPost()processPost()都涉及网络调用。

如果你有两个这样的实现,一个具有suspend funfunction,另一个具有常规的阻塞function,例如:

 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限制任务可能的优点,你需要一个用例,不管你是顺序执行还是并行执行都不是不相关的。 在上面的例子中,这被看作是一个不相关的内部细节:在一个版本中,你运行了1000个并发任务,而在另一个版本中,你只用了4个,所以它几乎是顺序执行。

Hazelcast Jet是这种用例的一个例子,因为计算任务是相互依赖的:输出是另一个输入。 在这种情况下,你不能只运行其中的一些,直到完成,在一个小的线程池,你实际上必须交错,所以缓冲输出不爆炸。 如果你试图建立这样一个有没有协程的场景,你会再一次发现你要分配的任务多,或者你正在使用可暂停的协程,而后一种方法会胜出。 Hazelcast Jet在普通的Java API中实现了协程的精神。 这在其参考手册中进行了讨论。 它的方法将从协程编程模型中受益匪浅,但目前它是纯Java的。

披露:这篇文章的作者属于Jet工程团队。

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