Kotlin:withContext() 与异步等待
Kotlin: withContext() vs Async-await
我一直在阅读 kotlin docs,如果我理解正确的话,这两个 Kotlin 函数的工作方式如下:
withContext(context)
:切换当前协程的上下文,当给定的块执行时,协程切换回之前的上下文。
async(context)
:在给定的上下文中启动一个新协程,如果我们在返回的 Deferred
任务上调用 .await()
,它将挂起调用协程并在块执行时恢复在生成的协程中 returns.
现在为code
以下两个版本:
版本 1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
版本 2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- 在两个版本中,block1()、block3() 在默认上下文(公共池?)中执行,而 block2() 在给定上下文中执行。
- 整体执行同步block1() -> block2() -> block3()顺序
- 我看到的唯一区别是版本 1 创建了另一个协程,而版本 2 在切换上下文时只执行一个协程。
我的问题是:
使用 withContext
而不是 async-await
不是总是更好吗,因为它在功能上相似,但不会创建另一个协程。大量协程虽然轻量级,但在要求苛刻的应用程序中仍然是一个问题。
有没有 async-await
比 withContext
更可取的情况?
更新:
Kotlin 1.2.50 现在有一个代码检查,它可以在其中转换 async(ctx) { }.await() to withContext(ctx) { }
.
Isn't it always better to use withContext rather than asynch-await as it is funcationally similar, but doesn't create another coroutine. Large numebrs coroutines, though lightweight could still be a problem in demanding applications
Is there a case asynch-await is more preferable to withContext
当你想同时执行多个任务时,你应该使用async/await,例如:
runBlocking {
val deferredResults = arrayListOf<Deferred<String>>()
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"1"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"2"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"3"
}
//wait for all results (at this point tasks are running)
val results = deferredResults.map { it.await() }
println(results)
}
如果您不需要同时运行多个任务,您可以使用 withContext。
Large number of coroutines, though lightweight, could still be a problem in demanding applications
我想通过量化他们的实际成本来消除 "too many coroutines" 成为问题的神话。
首先,我们应该将 coroutine 本身与它所附加的 coroutine context 分开。这就是您如何以最少的开销创建协程:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
这个表达式的值是一个 Job
持有一个挂起的协程。为了保留延续性,我们将其添加到范围更广的列表中。
我对这段代码进行了基准测试,得出的结论是它分配了 140 字节 并需要 100 纳秒 才能完成。这就是协程的轻量级。
为了可重复性,这是我使用的代码:
fun measureMemoryOfLaunch() {
val continuations = ContinuationList()
val jobs = (1..10_000).mapTo(JobList()) {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
}
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
class JobList : ArrayList<Job>()
class ContinuationList : ArrayList<Continuation<Unit>>()
此代码启动了一堆协同程序,然后休眠,因此您有时间使用 VisualVM 等监视工具分析堆。我创建了专门的 类 JobList
和 ContinuationList
因为这样可以更容易地分析堆转储。
为了获得更完整的故事,我使用下面的代码还测量了 withContext()
和 async-await
的成本:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis
const val JOBS_PER_BATCH = 100_000
var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()
fun main(args: Array<String>) {
try {
measure("just launch", justLaunch)
measure("launch and withContext", launchAndWithContext)
measure("launch and async", launchAndAsync)
println("Black hole value: $blackHoleCount")
} finally {
threadPool.shutdown()
}
}
fun measure(name: String, block: (Int) -> Job) {
print("Measuring $name, warmup ")
(1..1_000_000).forEach { block(it).cancel() }
println("done.")
System.gc()
System.gc()
val tookOnAverage = (1..20).map { _ ->
System.gc()
System.gc()
var jobs: List<Job> = emptyList()
measureTimeMillis {
jobs = (1..JOBS_PER_BATCH).map(block)
}.also { _ ->
blackHoleCount += jobs.onEach { it.cancel() }.count()
}
}.average()
println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}
fun measureMemory(name:String, block: (Int) -> Job) {
println(name)
val jobs = (1..JOBS_PER_BATCH).map(block)
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
val justLaunch: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {}
}
}
val launchAndWithContext: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
withContext(ThreadPool) {
suspendCoroutine<Unit> {}
}
}
}
val launchAndAsync: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
async(ThreadPool) {
suspendCoroutine<Unit> {}
}.await()
}
}
这是我从上面的代码得到的典型输出:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
是的,async-await
大约是 withContext
的两倍,但仍然只有一微秒。您必须在一个紧密的循环中启动它们,除此之外几乎什么都不做,才能在您的应用程序中成为 "a problem"。
使用 measureMemory()
我发现每次调用的内存成本如下:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
async-await
的开销正好比withContext
高140字节,我们得到的这个数字是一个协程的内存权重。这只是设置 CommonPool
上下文的全部成本的一小部分。
如果 performance/memory 影响是决定 withContext
和 async-await
之间的唯一标准,则结论必须是它们之间在 99% 的实际使用中没有相关差异案例。
真正的原因是withContext()
更简单直接API,尤其是在异常处理方面:
- 未在
async { ... }
中处理的异常导致其父作业被取消。无论您如何处理来自匹配 await()
的异常,都会发生这种情况。如果您还没有为它准备 coroutineScope
,它可能会拖垮您的整个应用程序。
withContext { ... }
中未处理的异常只会被 withContext
调用抛出,您可以像处理其他异常一样处理它。
withContext
也恰好进行了优化,利用了您暂停父协程并等待子协程的事实,但这只是一个额外的好处。
async-await
应该保留给那些你确实需要并发的情况,这样你就可以在后台启动多个协程,然后才等待它们。简而言之:
async-await-async-await
— 不要那样做,使用 withContext-withContext
async-async-await-await
— 这就是使用它的方式。
如有疑问,请记住以下规则:
如果多个任务必须并行发生并且最终结果取决于所有任务的完成,则使用async
.
返回单个任务的结果,使用withContext
。
我一直在阅读 kotlin docs,如果我理解正确的话,这两个 Kotlin 函数的工作方式如下:
withContext(context)
:切换当前协程的上下文,当给定的块执行时,协程切换回之前的上下文。async(context)
:在给定的上下文中启动一个新协程,如果我们在返回的Deferred
任务上调用.await()
,它将挂起调用协程并在块执行时恢复在生成的协程中 returns.
现在为code
以下两个版本:
版本 1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
版本 2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- 在两个版本中,block1()、block3() 在默认上下文(公共池?)中执行,而 block2() 在给定上下文中执行。
- 整体执行同步block1() -> block2() -> block3()顺序
- 我看到的唯一区别是版本 1 创建了另一个协程,而版本 2 在切换上下文时只执行一个协程。
我的问题是:
使用
withContext
而不是async-await
不是总是更好吗,因为它在功能上相似,但不会创建另一个协程。大量协程虽然轻量级,但在要求苛刻的应用程序中仍然是一个问题。有没有
async-await
比withContext
更可取的情况?
更新:
Kotlin 1.2.50 现在有一个代码检查,它可以在其中转换 async(ctx) { }.await() to withContext(ctx) { }
.
Isn't it always better to use withContext rather than asynch-await as it is funcationally similar, but doesn't create another coroutine. Large numebrs coroutines, though lightweight could still be a problem in demanding applications
Is there a case asynch-await is more preferable to withContext
当你想同时执行多个任务时,你应该使用async/await,例如:
runBlocking {
val deferredResults = arrayListOf<Deferred<String>>()
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"1"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"2"
}
deferredResults += async {
delay(1, TimeUnit.SECONDS)
"3"
}
//wait for all results (at this point tasks are running)
val results = deferredResults.map { it.await() }
println(results)
}
如果您不需要同时运行多个任务,您可以使用 withContext。
Large number of coroutines, though lightweight, could still be a problem in demanding applications
我想通过量化他们的实际成本来消除 "too many coroutines" 成为问题的神话。
首先,我们应该将 coroutine 本身与它所附加的 coroutine context 分开。这就是您如何以最少的开销创建协程:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
这个表达式的值是一个 Job
持有一个挂起的协程。为了保留延续性,我们将其添加到范围更广的列表中。
我对这段代码进行了基准测试,得出的结论是它分配了 140 字节 并需要 100 纳秒 才能完成。这就是协程的轻量级。
为了可重复性,这是我使用的代码:
fun measureMemoryOfLaunch() {
val continuations = ContinuationList()
val jobs = (1..10_000).mapTo(JobList()) {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
}
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
class JobList : ArrayList<Job>()
class ContinuationList : ArrayList<Continuation<Unit>>()
此代码启动了一堆协同程序,然后休眠,因此您有时间使用 VisualVM 等监视工具分析堆。我创建了专门的 类 JobList
和 ContinuationList
因为这样可以更容易地分析堆转储。
为了获得更完整的故事,我使用下面的代码还测量了 withContext()
和 async-await
的成本:
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis
const val JOBS_PER_BATCH = 100_000
var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()
fun main(args: Array<String>) {
try {
measure("just launch", justLaunch)
measure("launch and withContext", launchAndWithContext)
measure("launch and async", launchAndAsync)
println("Black hole value: $blackHoleCount")
} finally {
threadPool.shutdown()
}
}
fun measure(name: String, block: (Int) -> Job) {
print("Measuring $name, warmup ")
(1..1_000_000).forEach { block(it).cancel() }
println("done.")
System.gc()
System.gc()
val tookOnAverage = (1..20).map { _ ->
System.gc()
System.gc()
var jobs: List<Job> = emptyList()
measureTimeMillis {
jobs = (1..JOBS_PER_BATCH).map(block)
}.also { _ ->
blackHoleCount += jobs.onEach { it.cancel() }.count()
}
}.average()
println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}
fun measureMemory(name:String, block: (Int) -> Job) {
println(name)
val jobs = (1..JOBS_PER_BATCH).map(block)
(1..500).forEach {
Thread.sleep(1000)
println(it)
}
println(jobs.onEach { it.cancel() }.filter { it.isActive})
}
val justLaunch: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {}
}
}
val launchAndWithContext: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
withContext(ThreadPool) {
suspendCoroutine<Unit> {}
}
}
}
val launchAndAsync: (i: Int) -> Job = {
GlobalScope.launch(Dispatchers.Unconfined) {
async(ThreadPool) {
suspendCoroutine<Unit> {}
}.await()
}
}
这是我从上面的代码得到的典型输出:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
是的,async-await
大约是 withContext
的两倍,但仍然只有一微秒。您必须在一个紧密的循环中启动它们,除此之外几乎什么都不做,才能在您的应用程序中成为 "a problem"。
使用 measureMemory()
我发现每次调用的内存成本如下:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
async-await
的开销正好比withContext
高140字节,我们得到的这个数字是一个协程的内存权重。这只是设置 CommonPool
上下文的全部成本的一小部分。
如果 performance/memory 影响是决定 withContext
和 async-await
之间的唯一标准,则结论必须是它们之间在 99% 的实际使用中没有相关差异案例。
真正的原因是withContext()
更简单直接API,尤其是在异常处理方面:
- 未在
async { ... }
中处理的异常导致其父作业被取消。无论您如何处理来自匹配await()
的异常,都会发生这种情况。如果您还没有为它准备coroutineScope
,它可能会拖垮您的整个应用程序。 withContext { ... }
中未处理的异常只会被withContext
调用抛出,您可以像处理其他异常一样处理它。
withContext
也恰好进行了优化,利用了您暂停父协程并等待子协程的事实,但这只是一个额外的好处。
async-await
应该保留给那些你确实需要并发的情况,这样你就可以在后台启动多个协程,然后才等待它们。简而言之:
async-await-async-await
— 不要那样做,使用withContext-withContext
async-async-await-await
— 这就是使用它的方式。
如有疑问,请记住以下规则:
如果多个任务必须并行发生并且最终结果取决于所有任务的完成,则使用
async
.返回单个任务的结果,使用
withContext
。