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

2024-05-12

我编写了 3 个简单的程序来测试协程相对于线程的性能优势。每个程序都会执行许多常见的简单计算。所有程序都彼此分开运行。除了执行时间之外,我还通过以下方式测量了 CPU 使用率Visual VMIDE 插件。

  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()
    
  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()
    
  1. 第三个程序是用协程编写的,结果显示出很大的差异(来自41784 ms to 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 中实现的内容,但在实践中,我没有看到它们按预期工作。我的基准测试是否错误?或者也许我错误地使用了协程?


按照您设置问题的方式,您不应期望从协程中获得任何好处。在所有情况下,您都会向执行器提交不可分割的计算块。您没有利用协程挂起的想法,您可以在其中编写实际上被切碎并分段执行的顺序代码,可能在不同的线程上。

协程的大多数用例都围绕阻塞代码:避免占用线程只等待响应而不执行任何操作的情况。它们还可以用于交错 CPU 密集型任务,但这是一种更特殊的情况。

我建议对涉及多个连续阻塞步骤的 1,000,000 个任务进行基准测试,例如Roman Elizarov 在 KotlinConf 2017 上的演讲 https://youtu.be/_hfBv0a09Jc?t=10m12s:

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

其中所有的requestToken(), createPost() and processPost()涉及网络调用。

如果您有两种实现方式,其中一种是suspend funs 和另一个具有常规阻塞函数的函数,例如:

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

vs.

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

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

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

黑泽尔卡斯特喷射机 https://jet-start.sh/docs/architecture/execution-engine是此类用例的一个示例,因为计算任务是相互依赖的:一个的输出是另一个的输入。在这种情况下,您不能只运行其中的几个直到完成,在一个小线程池上,您实际上必须交错它们,以便缓冲的输出不会爆炸。如果您尝试使用或不使用协程来设置这样的场景,您将再次发现您要么分配与任务一样多的线程,要么使用可挂起的协程,而后一种方法会获胜。 Hazelcast Jet 在纯 Java API 中实现了协程的精神。它的方法将极大地受益于协程编程模型,但目前它是纯 Java 的。

Disclosure: the author of this post belongs to the Jet engineering team.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么线程比协程表现出更好的性能? 的相关文章

随机推荐