提问人:Criwran 提问时间:11/1/2023 最后编辑:Criwran 更新时间:11/2/2023 访问量:110
协程似乎并不比 JVM 线程占用更少的资源
Coroutines don't seem to be less resource-intensive than JVM threads
问:
我做了一个基准测试(参考答案)来测试线程池中 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
total: 561510
Dispatchers.IO 案例:
total: 262719
根据答案:
Coroutines are not designed to be faster than threads, it is for lower RAM consumption
有一个基准测试表明,线程和协程之间消耗的内存中似乎存在相对恒定的 6:1 比率。
但是从上面的图表中,我没有看到协程和线程池之间的内存使用有任何明显差异。
我设计的基准测试错了吗?
答:
TLDR:说协程比线程轻,我们通常意味着我们可以启动数千甚至数百万个并发协程——这是线程做不到的。我们可以回到旧的、易于使用的模型,即为每个任务启动一个新的“线程”。我们可以通过简单地启动更多的“线程”来将任务拆分为子任务。我们不会摆脱内存问题,所有协程将同时运行,我们不必显式地对任务进行排队,同时资源将得到最佳利用。
多年来,我们一直在努力并发处理以提供一些相互矛盾的属性:
- 可能一直使用所有 CPU(只要我们有事可做)。
- 保持较低的线程数,因为它们会引起开销:
- 空闲/等待线程 - 理想情况下,尽可能接近 0。它们消耗内存,什么都不做。
- 活动线程,尤其是 CPU 密集型线程,最多运行它们的 CPU 数量(或 2 个 CPU)。否则,线程会争夺对 CPU 的访问权,从而降低性能。
- 保持代码简单。
大多数并发模型都提供其中的 2 个属性,但会损害第三个属性。
您的示例未提供 1.您以一种非常低效的方式利用资源,因为您的应用程序几乎一直处于空闲状态,而它有一长串任务需要处理。
很久很久以前,我们会使用一个模型,在这个模型中,我们将为每个任务启动一个线程 - 它提供 1.和 3.,但显然不符合 2。
我们可以使用一个包含 50 个线程的线程池。这是一个非常容易做到的 (3.),通过选择线程数,我们可以在满足 1 之间取得平衡。和 2.较低的数字可能意味着所有线程都在等待,从而浪费 CPU。更高的数字意味着我们需要更多的线程内存,并且我们可能一次处理太多任务,因此它们必须争夺对 CPU 的访问权。此外,每个任务都必须为其数据分配一些内存,因此,最佳情况下,我们应该处理尽可能少的任务,以仅满足 1 个任务。对于线程池,我们无法完全控制这一点。我们只能尝试线程数以获得最佳折衷方案,但这需要一些工作,并且还远未达到最佳效果。
由于上述原因,我们开始使用异步模型:回调、期货、反应式流等。他们提供 1.和 2.,所以他们以非常有效的方式安排任务和使用资源,但他们对 3.
协程与其他异步模型几乎相同,它们对 1 具有非常相似的属性。和 2.,但它们允许保持代码像“很久很久以前”的情况一样简单 - 只需为每个任务生成一个协程,仅此而已。我们从上到下编写代码,我们可以在 CPU 密集型、在 I/O 中等待或等待与其他任务同步之间平稳切换,我们可以轻松地将任务拆分为并发子任务,然后再次加入,等等——所有这些都同时保持代码资源效率。
最直接的比较应该是协程和异步编程之间的比较。这两者的占用空间大致相同,并且在资源使用方面解决了相同的问题。简而言之,他们解决的问题是拥有大量并发任务,这些任务将大部分时间花在等待 IO 上。这个问题无法通过线程池和阻塞 IO 来解决。
现在,协程相对于异步编程的优势归结为代码简单性、可读性和范围。协程允许您编写简单的顺序代码,这些代码在内部转换为异步样式的编程风格。除了常规代码与嵌套回调的森林相比的明显优势之外,一个很大的优势是,您可以恢复自动向上传播调用堆栈的异常的简单性,而使用异步编程时,您必须一丝不苟地关注无处不在的 -type 回调,这些回调具有非常复杂的错误路径。OnError
评论
“Lightweight thread” means lightweight in comparison to spinning up a new thread per task
就是这样。我做了一个测试,与为每个任务创建一个线程相比,创建协程确实需要很少的内存。但是现在没有人会用 ,那样的话,就不认为协程在实际编程工作中有什么优势了,线程池就足够了。new Thread