为什么线程表现出比协程更好的性能?
Why threads are showing better performance than coroutines?
我写了 3 个简单的程序来测试协程相对于线程的性能优势。每个程序都会做很多常见的简单计算。所有程序都 运行 彼此分开。除了执行时间,我还通过 Visual VM
IDE 插件测量了 CPU 的使用情况。
第一个程序使用 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()
第二个程序具有相同的逻辑,但它只使用 n-threaded
池而不是 1000-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()
第三个程序是用协程编写的,结果差异很大(从 41784 ms
到 81101 ms
)。我很困惑,不太明白为什么它们如此不同以及为什么协程有时比线程慢(考虑到小型异步计算是协程的 forte)。这是代码:
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")
我实际上阅读了很多关于这些协程以及它们如何在 kotlin 中实现的内容,但实际上我没有看到它们按预期工作。我的基准测试做错了吗?或者也许我错误地使用了协程?
协程并不是为了比线程更快而设计的,它是为了降低 RAM 消耗和更好的异步调用语法。
按照您设置问题的方式,您不应期望从协程中获得任何好处。在所有情况下,您都将不可分割的计算块提交给执行者。您没有利用协程挂起的想法,您可以在其中编写实际被切碎并分段执行的顺序代码,可能在不同的线程上。
协程的大多数用例都围绕着阻塞代码:避免占用线程除了等待响应什么都不做的情况。它们也可以用于交错 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 fun
s,另一种带有常规阻塞函数,例如:
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 中实现了协同程序的精神。它的方法将极大地受益于协程编程模型,但目前它是纯粹的 Java.
披露:此 post 的作者属于 Jet 工程团队。
协程被设计为轻量级线程。它使用较低的 RAM,因为当您执行 1,000,000 个并发例程时,它不必创建 1,000,000 个线程。协程可以帮助你优化线程的使用,让执行更高效,你再也不用关心线程了。您可以将协程视为可运行对象或任务,您可以将其 post 放入处理程序并在线程或线程池中执行。
我写了 3 个简单的程序来测试协程相对于线程的性能优势。每个程序都会做很多常见的简单计算。所有程序都 运行 彼此分开。除了执行时间,我还通过 Visual VM
IDE 插件测量了 CPU 的使用情况。
第一个程序使用
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()
第二个程序具有相同的逻辑,但它只使用
n-threaded
池而不是1000-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()
第三个程序是用协程编写的,结果差异很大(从
41784 ms
到81101 ms
)。我很困惑,不太明白为什么它们如此不同以及为什么协程有时比线程慢(考虑到小型异步计算是协程的 forte)。这是代码: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")
我实际上阅读了很多关于这些协程以及它们如何在 kotlin 中实现的内容,但实际上我没有看到它们按预期工作。我的基准测试做错了吗?或者也许我错误地使用了协程?
协程并不是为了比线程更快而设计的,它是为了降低 RAM 消耗和更好的异步调用语法。
按照您设置问题的方式,您不应期望从协程中获得任何好处。在所有情况下,您都将不可分割的计算块提交给执行者。您没有利用协程挂起的想法,您可以在其中编写实际被切碎并分段执行的顺序代码,可能在不同的线程上。
协程的大多数用例都围绕着阻塞代码:避免占用线程除了等待响应什么都不做的情况。它们也可以用于交错 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 fun
s,另一种带有常规阻塞函数,例如:
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 中实现了协同程序的精神。它的方法将极大地受益于协程编程模型,但目前它是纯粹的 Java.
披露:此 post 的作者属于 Jet 工程团队。
协程被设计为轻量级线程。它使用较低的 RAM,因为当您执行 1,000,000 个并发例程时,它不必创建 1,000,000 个线程。协程可以帮助你优化线程的使用,让执行更高效,你再也不用关心线程了。您可以将协程视为可运行对象或任务,您可以将其 post 放入处理程序并在线程或线程池中执行。