[Coroutine] μ·¨μ†Œ

πŸ’‘ 쀑단 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” μ½”λ£¨ν‹΄μ—μ„œ μ·¨μ†Œλ₯Ό ν†΅ν•΄μ„œ μžμ› 낭비와 λ©”λͺ¨λ¦¬ λˆ„μˆ˜λ₯Ό 쀄일 수 μžˆλŠ” 방법에 λŒ€ν•˜μ—¬ ν•™μŠ΅ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

cancellation

  • 쀑단 ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λŠ” λͺ‡λͺ‡ ν΄λž˜μŠ€μ™€ λΌμ΄λΈŒλŸ¬λ¦¬λŠ” 항상 μ·¨μ†Œλ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.
  • λ‹¨μˆœνžˆ μŠ€λ ˆλ“œλ₯Ό 죽이면, 연결을 λ‹«κ³  μžμ›μ„ ν•΄μ œν•˜λŠ” κΈ°νšŒκ°€ μ—†κΈ° λ•Œλ¬Έμ— μ΅œμ•…μ˜ μ·¨μ†Œ 방법이 λ©λ‹ˆλ‹€.
  • κ°œλ°œμžλ“€μ΄ μƒνƒœκ°€ μ•‘ν‹°λΈŒν•œμ§€ ν™•μΈν•˜μ§€ μ•Šμ•„λ„ 되고, κ°„λ‹¨ν•˜κ³  μ•ˆμ „ν•œ λ°©μ‹μœΌλ‘œ μ·¨μ†Œν•  수 μžˆλ„λ‘ 코루틴은 이λ₯Ό μ§€μ›ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

기본적인 μ·¨μ†Œ

  • Job μΈν„°νŽ˜μ΄μŠ€λŠ” cancel λ©”μ„œλ“œλ₯Ό ν†΅ν•΄μ„œ μ·¨μ†Œλ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.

cancel()

  • 이λ₯Ό ν˜ΈμΆœν•œ 코루틴은 첫 번째 μ€‘λ‹¨μ μ—μ„œ μž‘μ„ λλƒ…λ‹ˆλ‹€.
  • 작이 μžμ‹μ„ 가지고 μžˆλ”°λ©΄ κ·Έλ“€ λ˜ν•œ μ·¨μ†Œλ˜λ©°, λΆ€λͺ¨λŠ” 영ν–₯을 받지 μ•ŠμŠ΅λ‹ˆλ‹€.
  • 작이 μ·¨μ†Œλ˜λ©΄, μ·¨μ†Œλœ μž‘μ€ μƒˆλ‘œμš΄ μ½”λ£¨ν‹΄μ˜ λΆ€λͺ¨λ‘œ μ‚¬μš©λ  수 μ—†μŠ΅λ‹ˆλ‹€.
  • μž‘μ„ μ·¨μ†Œν•˜λ©΄, Cancelling → Cancelled μƒνƒœλ‘œ λ³€ν•©λ‹ˆλ‹€.
val job = launch {
    repeat(1_000) { i ->
        delay(200)
        println("$i")
    }
}

delay(1100)
job.cancel()
job.join()

// 0
// 1
// 2
// 3
// 4
  • job.join()을 뒀에 μΆ”κ°€ν•˜λ©΄, 코루틴이 μ·¨μ†Œλ₯Ό 마칠 λ•ŒκΉŒμ§€ μ€‘λ‹¨λ˜λ―€λ‘œ 경쟁 μƒνƒœκ°€ λ°œμƒν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

cancelAndJoin()

  • kotlin.coroutines λΌμ΄λΈŒλŸ¬λ¦¬λŠ” cancelκ³Ό join을 ν•¨κ»˜ ν˜ΈμΆœν•  수 μžˆλŠ” ν™•μž₯ν•¨μˆ˜λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}
  • 이 λ°©λ²•μœΌλ‘œ Jobκ³Ό μ—°κ²° 된 μˆ˜λ§Žμ€ 코루틴을 ν•œλ²ˆμ— μ·¨μ†Œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • ν”Œλž«νΌμ˜ μ’…λ₯˜λ₯Ό λΆˆλ¬Έν•˜κ³  λ™μ‹œμ— μˆ˜ν–‰λ˜λŠ” μž‘μ—… κ·Έλ£Ή 전체λ₯Ό μ·¨μ†Œμ‹œμΌœμ•Ό ν•  λ•Œκ°€ μžˆμŠ΅λ‹ˆλ‹€.
  • μ•ˆλ“œλ‘œμ΄λ“œλ‘œ 예λ₯Ό λ“€λ©΄ μ‚¬μš©μžκ°€ λ·° 창을 λ‚˜κ°”μ„ λ•Œ, λ·°μ—μ„œ μ‹œμž‘λœ λͺ¨λ“  코루틴을 μ·¨μ†Œν•˜λŠ” κ²½μš°μž…λ‹ˆλ‹€.

μ·¨μ†Œμ˜ μž‘λ™ 방식

  • 작이 μ·¨μ†Œλ˜λ©΄ Cancelling μƒνƒœλ‘œ λ°”λ€λ‹ˆλ‹€.
  • μƒνƒœκ°€ 바뀐 λ’€ 첫 번째 μ€‘λ‹¨μ μ—μ„œ CancellationException μ˜ˆμ™Έλ₯Ό λ˜μ§€λ©°, μ˜ˆμ™Έλ₯Ό μž‘κ±°λ‚˜ λ‹€μ‹œ 던질 수 μžˆμŠ΅λ‹ˆλ‹€.
// in coroutineScope
val job = Job()
launch(job){
    try {
        repeat(1_000) { i ->
            delay(200)
            println("$i")
        }
    } catch (e : CancellationException) {
        println(e)
        throw e
    }
}
job.cancelAndJoin()
// JobCancellationException

μ·¨μ†Œ 쀑 코루틴 ν•œ 번 더 ν˜ΈμΆœν•˜κΈ°

  • 코루틴이 μ‹€μ œλ‘œ μ’…λ£Œλ˜κΈ° 전에 CancellationException을 작고, μ’€ 더 λ§Žμ€ 연산이 μˆ˜ν–‰ κ°€λŠ₯ν•©λ‹ˆλ‹€.
  • ν•˜μ§€λ§Œ μ •λ¦¬ κ³Όμ • 쀑에 쀑단을 ν—ˆμš©ν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€.
  • Job이 이미 Cancelling μƒνƒœκ°€ λ˜μ—ˆμ„ λ•Œ, μ€‘λ‹¨ν•˜κ±°λ‚˜ λ‹€λ₯Έ 코루틴을 μ‹œμž‘ν•˜λŠ” 것은 λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€.
  • 이런 경우 withContextλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • 이미 코루틴이 μ·¨μ†Œλ˜μ—ˆμ„ λ•Œ 쀑단 ν•¨μˆ˜λ₯Ό λ°˜λ“œμ‹œ ν˜ΈμΆœν•΄μ•Ό ν•˜λŠ” κ²½μš°κ°€ λ°œμƒ
    • λ°μ΄ν„°λ² μ΄μŠ€μ˜ λ³€κ²½ 사항을 λ‘€λ°±ν•΄μ•Ό ν•˜λŠ” 경우
    • μ΄λŠ” μ½”λ“œ λΈ”λ‘μ˜ μ»¨ν…μŠ€νŠΈλ₯Ό λ°”κΎΈλŠ” 것이며, withContext λ‚΄λΆ€μ—μ„œλŠ” μ·¨μ†Œλ  수 μ—†λŠ” Job인 NonCancellable 객체λ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • 이 λ•Œ 블둝 λ‚΄λΆ€μ—μ„œ μž‘μ€ μ•‘ν‹°λΈŒ μƒνƒœλ₯Ό μœ μ§€ν•˜λ©°, 쀑단 ν•¨μˆ˜λ₯Ό μ›ν•˜λŠ” 만큼 ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€.
val job = Job()
launch(job) {
    try {
        delay(200)
        println("Coroutine finished")
    } finally {
        println("Finally")
        withContext(NonCancellable){
            delay(1000L)
            println("Cleanup Done")
        }
    }
}
delay(100)
job.cancelAndJoin()
println("Done")

// Finally
// Cleanup Done
// Done

invokeOnCompletion()

  • μžμ›μ„ ν•΄μ œν•˜λŠ” 데 자주 μ‚¬μš©λ˜λŠ” Job의 λ©”μ„œλ“œμž…λ‹ˆλ‹€.
  • 작이 Completedλ‚˜ Cancelled와 같은 λ§ˆμ§€λ§‰ μƒνƒœμ— λ„λ‹¬ν–ˆμ„ λ•Œ 호좜될 ν•Έλ“€λŸ¬λ₯Ό μ§€μ •ν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€.
val job = launch {
    delay(1000)
}
job.invokeOnCompletion { exception: Throwable? ->
    println("Finished")
}
delay(400)
job.cancelAndJoin()

// Finished
  • ν•Έλ“€λŸ¬μ˜ νŒŒλΌλ―Έν„° 쀑 ν•˜λ‚˜μΈ μ˜ˆμ™Έμ˜ μ’…λ₯˜λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.
    • 작이 μ˜ˆμ™Έ 없이 λλ‚˜λ©΄ null이 λ©λ‹ˆλ‹€.
    • 코루틴이 μ·¨μ†Œλ˜μ—ˆμœΌλ©΄ CancellationException이 λ©λ‹ˆλ‹€.
    • 코루틴을 μ’…λ£Œμ‹œν‚¨ μ˜ˆμ™Έκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • 이 ν•¨μˆ˜λŠ” μ·¨μ†Œν•˜λŠ” 쀑에 λ™κΈ°μ μœΌλ‘œ 호좜되며, μ–΄λ–€ μŠ€λ ˆλ“œμ—μ„œ 싀행할지 κ²°μ •ν•  μˆ˜λŠ” μ—†μŠ΅λ‹ˆλ‹€.

쀑단될 수 μ—†λŠ” κ±Έ μ€‘λ‹¨ν•˜κΈ°

  • μ·¨μ†ŒλŠ” μ€‘λ‹¨μ μ—μ„œ μΌμ–΄λ‚˜κΈ° λ•Œλ¬Έμ— 쀑단점이 μ—†μœΌλ©΄ μ·¨μ†Œλ₯Ό ν•  수 μ—†μŠ΅λ‹ˆλ‹€.
  • 쀑단을 μœ„ν•΄μ„œ Thread.sleep을 μ‚¬μš©ν•  수 μžˆμ§€λ§Œ, μ΄λŠ” 쒋지 μ•Šμ€ 방법이며 이런 상황을 λŒ€μ²΄ν•˜λŠ” μ—¬λŸ¬κ°€μ§€ 방법이 μžˆμŠ΅λ‹ˆλ‹€.

yield()λ₯Ό 주기적으둜 호좜

  • yieldλŠ” 코루틴을 μ€‘λ‹¨ν•˜κ³  μ¦‰μ‹œ μž¬μ‹€ν–‰ν•©λ‹ˆλ‹€.
  • 쀑단점이 생겼기 λ•Œλ¬Έμ— μ·¨μ†Œλ₯Ό 포함해 쀑단 쀑에 ν•„μš”ν•œ λͺ¨λ“  μž‘μ—…μ„ ν•  수 μžˆλŠ” κΈ°νšŒκ°€ μ£Όμ–΄μ§‘λ‹ˆλ‹€.
  • 쀑단 κ°€λŠ₯ν•˜μ§€ μ•ŠμœΌλ©΄μ„œ CPU μ§‘μ•½μ μ΄κ±°λ‚˜ μ‹œκ°„ 집약적인 연산듀이 쀑단 ν•¨μˆ˜μ— μžˆλ‹€λ©΄, 각 μ—°μ‚°λ“€ 사이에 yieldλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.
suspend fun cpuIntensiveOperations() =
    withContext(Dispatchers.Default) {
        cpuIntensiveOperation1()
        yield()
        cpuIntensiveOperation2()
        yield()
        cpuIntensiveOperation3()
    }

작의 μƒνƒœλ₯Ό 좔적

  • 코루틴 λΉŒλ” λ‚΄λΆ€μ—μ„œ thisλŠ” λΉŒλ”μ˜ μŠ€μ½”ν”„λ₯Ό μ°Έμ‘°ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • coroutineContext[Job] λ˜λŠ” coroutineContext.job을 ν†΅ν•΄μ„œ ν˜„μž¬ μƒνƒœκ°€ 무엇인지 확인할 수 있으며, 이λ₯Ό 톧해 μ•‘ν‹°λΈŒν•œμ§€ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.
// isActive
public val CortoineScpoe.isActive : Boolean
    get() = coroutineContext[Job]?.isActive ?: true

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job){
        do {
            // μƒνƒœκ°€ μ•‘ν‹°λΈŒν•  λ•Œ 
        } while(isActivte)
    }
}
  • λ˜λŠ” ensureActive() ν•¨μˆ˜λ₯Ό ν™œμš©ν•΄μ„œ Job이 μ•‘ν‹°λΈŒν•œ μƒνƒœκ°€ μ•„λ‹ˆλ©΄ CancellationException을 던질 수 μžˆμŠ΅λ‹ˆλ‹€.
suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        Thread.sleep(200)
        ensureActive()
    }
    delay(100)
    job.cancelAndJoin()
}

yield()와 ensureActive()의 차이

  • 두 ν•¨μˆ˜ λͺ¨λ‘ λ‹€λ₯Έ 코루틴이 μ‹€ν–‰ν•  수 μžˆλŠ” 기회λ₯Ό μ€€λ‹€λŠ” μ μ—μ„œ κ²°κ³ΌλŠ” λΉ„μŠ·ν•˜μ§€λ§Œ, 맀우 λ‹€λ₯Έ κΈ°λŠ₯을 ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • ensuereActive() ν•¨μˆ˜λŠ” λ°˜λ“œμ‹œ CoroutineScope(λ˜λŠ” CoroutineContextλ‚˜ Job)μ—μ„œ ν˜ΈμΆœλ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.
    • 일반적으둜 μ’€ 더 κ°€λ²Όμš΄ μž‘μ—…μ΄κΈ° λ•Œλ¬Έμ— 자주 μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • yield()λŠ” μ „ν˜•μ μΈ μ΅œμƒμœ„ 쀑단 ν•¨μˆ˜μž…λ‹ˆλ‹€.
    • μŠ€μ½”ν”„κ°€ ν•„μš”ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 일반적인 쀑단 ν•¨μˆ˜μ—μ„œλ„ μ‚¬μš©λ  수 μžˆμ§€λ§Œ, λ””μŠ€νŒ¨μ²˜λ₯Ό μ‚¬μš©ν•  λ•Œ μŠ€λ ˆλ“œκ°€ λ°”λ€ŒλŠ” λ¬Έμ œκ°€ 생길 수 μžˆμŠ΅λ‹ˆλ‹€.
    • λ•Œλ¬Έμ— CPU μ‚¬μš©λŸ‰μ΄ ν¬κ±°λ‚˜ μŠ€λ ˆλ“œλ₯Ό λΈ”λ‘œν‚Ήν•˜λŠ” 쀑단 ν•¨μˆ˜μ—μ„œ 자주 μ‚¬μš©λ©λ‹ˆλ‹€.

suspendCancellableCoroutine

  • 이 ν•¨μˆ˜λŠ” suspendCoroutineκ³Ό λΉ„μŠ·ν•˜μ§€λ§Œ, μ»¨ν‹°λ‰΄μ—μ΄μ…˜ 객체λ₯Ό λͺ‡ 가지 λ©”μ„œλ“œκ°€ μΆ”κ°€λœ CancellableContinuation<T>둜 λž˜ν•‘ν•©λ‹ˆλ‹€.
  • κ°€μž₯ μ€‘μš”ν•œ λ©”μ„œλ“œλŠ” 코루틴이 μ·¨μ†Œλ˜μ—ˆμ„ λ•Œ 행동을 μ •μ˜ν•˜λŠ” 데 μ‚¬μš©ν•˜λŠ” invokeOnCancellation λ©”μ„œλ“œμž…λ‹ˆλ‹€.
  • 이 λ©”μ„œλ“œλŠ” 라이브러리의 싀행을 μ·¨μ†Œν•˜κ±°λ‚˜ μžμ›μ„ ν•΄μ œν•  λ•Œ 주둜 μ‚¬μš©ν•©λ‹ˆλ‹€.
suspend fun someTask() = suspendCancellableCoroutine { cont ->
    cont.invokeOnCancellation {
        // 정리 μž‘μ—…μ„ μˆ˜ν–‰
    }
    // λ‚˜λ¨Έμ§€ κ΅¬ν˜„ λΆ€λΆ„
}
  • λ˜ν•œ CancellableContinuation<T>μ—μ„œλ„ (isActive, isCompleted, isCancelled ν”„λ‘œνΌν‹°λ₯Ό μ‚¬μš©) 작의 μƒνƒœλ₯Ό 확인할 수 있으며, μ»¨ν‹°λ‰΄μ—μ΄μ…˜μ„ μ·¨μ†Œν•  λ•Œ μ·¨μ†Œκ°€ λ˜λŠ” 원인을 μΆ”κ°€μ μœΌλ‘œ μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μš”μ•½

  • μ·¨μ†ŒλŠ” μ½”λ£¨ν‹΄μ˜ κ°•λ ₯ν•˜κ³  μ€‘μš”ν•œ κΈ°λŠ₯이며, μ‚¬μš©ν•˜λŠ” 데 κΉŒλ‹€λ‘œμš΄ κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€.
  • ν•˜μ§€λ§Œ μ·¨μ†Œλ₯Ό μ μ ˆν•˜κ²Œ μ‚¬μš©ν•˜λ©΄ μžμ› 낭비와 λ©”λͺ¨λ¦¬ λˆ„μˆ˜λ₯Ό 쀄일 수 μžˆμŠ΅λ‹ˆλ‹€.