Development Tip

Kotlin : withContext () 대 Async-await

yourdevel 2020. 11. 24. 19:58
반응형

Kotlin : withContext () 대 Async-await


나는 kotlin 문서 이며 올바르게 이해하면 두 개의 kotlin 함수가 다음과 같이 작동합니다.

  1. withContext(context): 현재 코 루틴의 컨텍스트를 전환합니다. 주어진 블록이 실행되면 코 루틴이 이전 컨텍스트로 다시 전환됩니다.
  2. async(context): 주어진 컨텍스트에서 새 코 루틴을 시작 .await()하고 반환 된 Deferred작업을 호출하면 호출 된 코 루틴을 일시 중단하고 생성 된 코 루틴 내부에서 실행중인 블록이 반환 될 때 다시 시작합니다.

이제 다음 두 가지 버전이 있습니다 code.

버전 1 :

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }

버전 2 :

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. 두 버전 모두 block1 (), block3 ()은 기본 컨텍스트 (commonpool?)에서 실행됩니다. 여기서 as blocks ()은 주어진 컨텍스트에서 실행됩니다.
  2. 전체 실행은 block1 ()-> block2 ()-> block3 () 순서와 동기화됩니다.
  3. 내가 보는 유일한 차이점은 version1이 다른 코 루틴을 생성한다는 것입니다. 여기서 version2는 컨텍스트를 전환하는 동안 하나의 코 루틴 만 실행합니다.

내 질문은 다음과 같습니다.

  1. 기능적으로 비슷 하기 withContext보다는 항상 사용하는 것이 async-await좋지만 다른 코 루틴을 만들지는 않습니다. 많은 수의 코 루틴은 가볍지 만 까다로운 애플리케이션에서는 여전히 문제가 될 수 있습니다.

  2. async-await더 바람직한 경우 withContext있습니까?

업데이트 : Kotlin 1.2.50 에는 이제 async(ctx) { }.await() to withContext(ctx) { }.


많은 수의 코 루틴은 가볍지 만 까다로운 응용 프로그램에서 여전히 문제가 될 수 있습니다.

실제 비용을 정량화하여 "너무 많은 코 루틴"이 문제가된다는이 신화를 없애고 싶습니다.

먼저 코 루틴 이 연결된 코 루틴 컨텍스트 에서 코 루틴 자체를 분리해야합니다 . 이것은 최소한의 오버 헤드로 코 루틴을 만드는 방법입니다.

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약 2 배 정도 걸리지 withContext만 여전히 1 마이크로 초입니다. 앱에서 "문제"가 되려면 그 외에는 거의 아무것도하지 않고 타이트한 루프로 실행해야합니다.

사용 measureMemory()하여 다음과 같은 호출 당 메모리 비용을 찾았습니다.

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

The cost of async-await is exactly 140 bytes higher than withContext, the number we got as the memory weight of one coroutine. This is just a fraction of the complete cost of setting up the CommonPool context.

If performance/memory impact was the only criterion to decide between withContext and async-await, the conclusion would have to be that there's no relevant difference between them in 99% of real use cases.

The real reason is that withContext() a simpler and more direct API, especially in terms of exception handling:

  • An exception that isn't handled within async { ... } causes its parent job to get cancelled. This happens regardless of how you handle exceptions from the matching await(). If you haven't prepared a coroutineScope for it, it may bring down your entire application.
  • An exception not handled within withContext { ... } simply gets thrown by the withContext call, you handle it just like any other.

withContext also happens to be optimized, leveraging the fact that you're suspending the parent coroutine and awaiting on the child, but that's just an added bonus.

async-await should be reserved for those cases where you actually want concurrency, so that you launch several coroutines in the background and only then await on them. In short:

  • async-await-async-await — same as withContext-withContext
  • async-async-await-await — that's the way to use it.

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

You should use async/await when you want to execute multiple tasks concurrently, for example:

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)
}

If you don't need to run multiple tasks concurrently you can use withContext.


When in doubt, remember this like a rule of thumb:

  1. If multiple tasks have to happen in parallel and final result depends on completion of all of them, then use async.

  2. For returning the result of a single task, use withContext.

참고URL : https://stackoverflow.com/questions/50230466/kotlin-withcontext-vs-async-await

반응형