协程似乎并不比 JVM 线程占用更少的资源

Cri*_*ran 1 java multithreading coroutine kotlin kotlin-coroutines

我做了一个基准测试(参考答案来测试线程池中coroutiens和线程之间的内存使用情况:

val COUNT = 4_000

val executor: Executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())// 12 cores on my machine
 fun main(array: Array<String>)  = runBlocking{
     val latch = CountDownLatch(COUNT)
     val start = System.currentTimeMillis()
     repeat(COUNT) {
         launch(Dispatchers.Default) {
             testByCoroutine(latch)
         }
     }
     latch.await()
     println("total: " + (System.currentTimeMillis() - start))


     // testByThreadPool()
 }


fun testByThreadPool() {
    val latch = CountDownLatch(COUNT)
    val start = System.currentTimeMillis()
    for (i in 0..<COUNT) {
        executor.execute {
            val num: Int = request1()
            println(request2(num))
            latch.countDown()
        }
    }
    try {
        latch.await()
    } catch (e: InterruptedException) {
        throw RuntimeException(e)
    }
    println("total: " + (System.currentTimeMillis() - start))
    exitProcess(0)
}

fun testByCoroutine(latch: CountDownLatch) {
    val num = request1()
    val res: Int = request2(num)
    println(res)
    latch.countDown()
}


fun request1(): Int {
    return doGet("https://bing.com")
}

fun request2(token: Int): Int {
    val response: Int = doGet("https://bing.com")
    return response + token
}

fun doGet(link: String): Int {
    try {
        val url = URL(link)
        val conn = url.openConnection() as HttpURLConnection
        conn.setRequestMethod("GET")
        conn.setConnectTimeout(3000)
        return conn.getResponseCode()
    } catch (e: java.lang.Exception) {
        e.printStackTrace()
    }
    return -1
}
Run Code Online (Sandbox Code Playgroud)

线程池案例:

total: 518218
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述 Dispatchers.Default 情况:

total: 561510
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

Dispatchers.IO 案例:

total: 262719
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

根据答案

Coroutines are not designed to be faster than threads, it is for lower RAM consumption 
Run Code Online (Sandbox Code Playgroud)

有一个基准测试表明,线程和协程之间消耗的内存似乎有相对恒定的 6:1 比例。

但从上图中我没有看到协程和线程池之间的内存使用有任何明显的差异。

我设计的基准是错误的吗?

bro*_*oot 7

TLDR:通过说协程比线程更轻,我们通常意味着我们可以启动数千甚至数百万个并发协程 - 这是我们无法使用线程做到的。我们可以回到旧的、易于使用的模型,为每个任务启动一个新的“线程”。我们可以通过简单地启动更多“线程”来将任务拆分为子任务。我们不会出现内存不足的问题,所有协程将同时运行,我们不必显式地对任务进行排队,同时资源将得到最佳利用。

多年来,我们在并发处理中努力提供有些矛盾的属性:

  1. 尽可能始终利用所有 CPU(只要我们有事情要做)。
  2. 保持较低的线程数量,因为它们会产生开销:
  • 空闲/等待线程 - 理想情况下,尽可能接近 0。它们消耗内存,但什么也不做。
  • 活动线程,尤其是 CPU 密集型线程 - 最多运行其中的 CPU 数量(或 2x CPU)。否则,线程会争夺对 CPU 的访问权,从而降低性能。
  1. 保持代码简单。

大多数并发模型提供了其中两个属性,但牺牲了第三个属性。

您的示例未提供 1. 您以非常低效的方式利用资源,因为您的应用程序几乎一直处于空闲状态,而它有很长的任务队列需要处理。

很久很久以前,我们会使用一种模型,其中每个任务启动一个线程 - 它提供了 1. 和 3.,但显然不满足 2.

我们可以使用一个包含 50 个线程的线程池。这是非常容易做到的 (3.),通过选择线程数,我们可以在满足 1. 和 2. 之间进行平衡。较低的数字可能意味着所有线程都在等待,浪费 CPU。数字越大意味着我们需要更多的内存用于线程,并且我们可能会同时处理太多任务,因此它们必须争夺对 CPU 的访问权限。另外,每个任务都必须为其数据分配一些内存,因此,最佳情况下,我们应该处理尽可能少的任务,仅满足 1。使用线程池,我们无法完全控制这一点。我们只能尝试线程数量来获得最佳折衷方案,但这需要一些工作,而且距离最佳还很远。

由于上述原因,我们开始使用异步模型:回调、future、反应流等。它们提供了 1. 和 2.,因此它们以非常高效的方式调度任务和使用资源,但对于 3. 来说它们很糟糕。

协程在底层的作用与其他异步模型几乎相同,它们具有与 1. 和 2. 非常相似的属性,但它们允许保持代码像“很久很久以前”的情况一样简单 - 只需为每个任务生成一个协程就是这样。我们从上到下编写代码,我们可以在CPU密集型、等待I/O或等待与其他任务同步之间平滑切换,我们可以轻松地将一个任务拆分为并发子任务,然后再次加入等等 - all同时仍然保持代码资源效率。