Kotlin : withContext () 대 Async-await


91

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?)에서 실행됩니다. 여기서 block2 ()는 주어진 컨텍스트에서 실행됩니다.
  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) { }.


을 사용하면 withContext항상 새로운 코 루틴이 생성됩니다. 이것이 소스 코드에서 볼 수있는 것입니다.
stdout

@stdout async/awaitOP에 따르면 새로운 코 루틴도 생성 하지 않습니까?
IgorGanapolsky

답변:


126

많은 수의 코 루틴은 가볍지 만 까다로운 애플리케이션에서는 여전히 문제가 될 수 있습니다.

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

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

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

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

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

비용은 코 루틴 하나의 메모리 가중치로 얻은 숫자 async-await보다 정확히 140 바이트 더 높습니다 withContext. 이는 CommonPool컨텍스트 를 설정하는 데 드는 전체 비용의 일부에 불과합니다 .

성능 / 메모리 영향이 withContext와 사이를 결정하는 유일한 기준 인 async-await경우 실제 사용 사례의 99 %에서 둘 사이에 관련성있는 차이가 없다는 결론이 내려야 합니다.

진짜 이유는 withContext()특히 예외 처리 측면에서 더 간단하고 직접적인 API이기 때문입니다 .

  • 내부에서 처리되지 않는 예외로 async { ... }인해 상위 작업이 취소됩니다. 이것은 일치하는 예외를 처리하는 방법에 관계없이 발생합니다 await(). 준비하지 않은 경우 coroutineScope전체 응용 프로그램이 중단 될 수 있습니다.
  • 내에서 처리되지 않은 예외는 withContext { ... }단순히 withContext호출에 의해 throw되며 다른 것과 마찬가지로 처리합니다.

withContext 또한 부모 코 루틴을 일시 중단하고 자식을 기다리고 있다는 사실을 활용하여 최적화되지만 이는 추가 보너스 일뿐입니다.

async-await실제로 동시성을 원하는 경우를 위해 예약해야합니다. 그래야 백그라운드에서 여러 코 루틴을 시작한 다음 기다릴 수 있습니다. 요컨대 :

  • async-await-async-await —하지 마십시오. withContext-withContext
  • async-async-await-await — 그것이 그것을 사용하는 방법입니다.

의 추가 메모리 비용과 관련하여 async-await: 우리가 사용할 때 withContext새로운 코 루틴도 생성됩니다 (소스 코드에서 볼 수있는 한), 차이가 다른 곳에서 올 수 있다고 생각하십니까?
stdout

1
@stdout이 테스트를 실행 한 이후로 라이브러리가 진화하고 있습니다. 답변의 코드는 완전히 독립적이어야합니다. 다시 실행하여 유효성을 검사하십시오. 차이의 일부를 설명 할 수 async있는 Deferred객체를 만듭니다 .
Marko Topolnik

~ " 계속 유지하려면 ". 언제 이것을 유지해야합니까?
IgorGanapolsky

1
@IgorGanapolsky 항상 유지되지만 일반적으로 사용자에게 표시되지는 않습니다. 연속을 잃는 것은 Thread.destroy()실행이 허공으로 사라지는 것과 같습니다 .
Marko Topolnik

22

기능적으로 유사하지만 다른 코 루틴을 생성하지 않기 때문에 asynch-await보다는 withContext를 사용하는 것이 항상 낫지 않습니까? 까다로운 응용 프로그램에서는 경량이 여전히 문제가 될 수 있지만 큰 numebrs 코 루틴

asynch-await가 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를 사용할 수 있습니다.


13

의심 스러우면 이것을 경험의 법칙처럼 기억하십시오.

  1. 여러 작업이 병렬로 발생해야하고 최종 결과가 모든 작업의 ​​완료에 따라 달라지는 경우 async.

  2. 단일 작업의 결과를 반환하려면 withContext.


1
asyncwithContext차단 이 모두 일시 중지 범위에 있습니까?
IgorGanapolsky

3
@IgorGanapolsky 당신은 메인 스레드의 차단에 대해 이야기하는 경우 asyncwithContext일부 긴 실행 작업이 실행하고 결과를 기다리는 동안 주 스레드를 차단하지 않습니다, 그들은 단지 코 루틴의 몸을 일시 중단합니다. 자세한 내용과 예제는 Medium : Async Operations with Kotlin Coroutines 에서이 문서를 참조하세요 .
Yogesh Umesh Vaity
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.