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

Coroutines don't seem to be less resource-intensive than JVM threads

提问人:Criwran 提问时间:11/1/2023 最后编辑:Criwran 更新时间:11/2/2023 访问量:110

问:

我做了一个基准测试(参考答案)来测试线程池中 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
}

线程池案例:

total: 518218

enter image description hereDispatchers.Default 案例:

total: 561510

enter image description here

Dispatchers.IO 案例:

total: 262719

enter image description here

根据答案

Coroutines are not designed to be faster than threads, it is for lower RAM consumption 

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

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

我设计的基准测试错了吗?

java 多线程 kotlin kotlin-coroutines 协程

评论

4赞 Tenfour04 11/1/2023
与线程池相比,协程不提供内存或速度优势。“轻量级线程”意味着与每个任务启动一个新线程相比,它是轻量级的,这在使用线程池时是没有的。与基本线程池相比,协程引擎盖下的延续机制是一个额外的负担。
1赞 Criwran 11/1/2023
“Lightweight thread” means lightweight in comparison to spinning up a new thread per task就是这样。我做了一个测试,与为每个任务创建一个线程相比,创建协程确实需要很少的内存。但是现在没有人会用 ,那样的话,就不认为协程在实际编程工作中有什么优势了,线程池就足够了。new Thread
0赞 Criwran 11/1/2023
但是异步调用的协程语法非常好
2赞 Tenfour04 11/2/2023
它主要用于结构化并发。语法也是一个巨大的好处。不再有“回调地狱”。

答:

7赞 broot 11/1/2023 #1

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

多年来,我们一直在努力并发处理以提供一些相互矛盾的属性:

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

大多数并发模型都提供其中的 2 个属性,但会损害第三个属性。

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

很久很久以前,我们会使用一个模型,在这个模型中,我们将为每个任务启动一个线程 - 它提供 1.和 3.,但显然不符合 2。

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

由于上述原因,我们开始使用异步模型:回调、期货、反应式流等。他们提供 1.和 2.,所以他们以非常有效的方式安排任务和使用资源,但他们对 3.

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

1赞 Marko Topolnik 11/2/2023 #2

最直接的比较应该是协程和异步编程之间的比较。这两者的占用空间大致相同,并且在资源使用方面解决了相同的问题。简而言之,他们解决的问题是拥有大量并发任务,这些任务将大部分时间花在等待 IO 上。这个问题无法通过线程池和阻塞 IO 来解决。

现在,协程相对于异步编程的优势归结为代码简单性、可读性和范围。协程允许您编写简单的顺序代码,这些代码在内部转换为异步样式的编程风格。除了常规代码与嵌套回调的森林相比的明显优势之外,一个很大的优势是,您可以恢复自动向上传播调用堆栈的异常的简单性,而使用异步编程时,您必须一丝不苟地关注无处不在的 -type 回调,这些回调具有非常复杂的错误路径。OnError