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

时间:2018-01-05 02:05:20

标签: kotlin benchmarking kotlin-coroutines

我编写了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()
    
  2. 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()
      
    2. second program

      1. 第三个程序是用协同程序编写的,结果显示差异很大(从41784 ms81101 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")
        
      2. third program

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

3 个答案:

答案 0 :(得分:14)

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

大多数协同程序的使用案例都围绕阻塞代码:避免让一个线程不做任何事情而是等待响应的情况。它们也可能用于交错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"
}

VS

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工程团队。

答案 1 :(得分:5)

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

答案 2 :(得分:0)

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