ABOUT ME

-

  • Kotlin) Coroutine 공식 가이드 번역 02 - Cancellation and Timeouts
    Kotlin 2021. 1. 11. 18:22


    코루틴 실행 취소하기

    장시간 구동되는 어플리케이션에서는 백그라운드 코루틴에 대한 세밀한 제어가 필요합니다.

    사용자가 코루틴을 시작시킨 페이지를 닫았을 수 있으며, 이제 그 결과를 필요로 하지 않고 해당 작업을 취소할 수 있습니다.

    launch함수는 실행중인 코루틴을 취소시키는 데 사용할 수 있는 job을 리턴시킵니다.

    code
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 약간 delay
    println("main: I'm tired of waiting!")
    job.cancel() // job 취소
    job.join() // job의 완료를 대기
    println("main: Now I can quit.")
    result
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    main: Now I can quit.

    메인 함수에서 job.cancel을 호출하는 즉시 다른 코루틴(job)이 취소되었기 때문에 더 이상 출력되지 않습니다.

    job에는 cancel과 join을 결합시킨 cancelAndJoin 확장 함수도 존재합니다.

     


    취소는 협조적

    코루틴 취소는 협조적이며 코루틴 코드는 취소 가능하도록 협조해야 합니다.

    모든 kotlinx.coroutinessuspending function은 취소가능합니다. 이 들은 코루틴의 취소여부를 확인하고, 취소 되었을 때 Cancellation Exception을 던집니다. 그러나 코루틴이 계산 작업을 하고 있으며 취소여부를 확인하지 않으면, 취소할 수 없습니다.

     

    code
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // CPU만 낭비하는 computation loop
            // 초당 메시지 두 번 출력
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 약간 delay
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // job 취소 후, 완료될 때까지 대기
    println("main: Now I can quit.")
    result
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    job: I'm sleeping 3 ...
    job: I'm sleeping 4 ...
    main: Now I can quit.

    계산 코드 취소 가능하도록 만들기

    계산 코드를 취소가능하도록 만드는 2가지 접근법입니다.

     

    1. 취소여부를 확인하는 suspending function을 주기적으로 호출 -> yield 함수 존재

    2. 명시적으로 취소여부 확인 (isActive 이용)

     

    code
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 취소가능한 computation loop
            // 초당 메시지 두 번 출력
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // 약간 delay
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // job 취소 후, 완료될 때까지 대기
    println("main: Now I can quit.")
    result
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    main: Now I can quit.

    취소가 되었습니다. isActiveCoroutineScope 객체를 통해 코루틴 내에서 실행가능한 확장 프로퍼티입니다.


    finally를 사용하여 리소스 닫기

    취소가능한 suspending function은 취소 시, Cancellation Exception을 던지며 일반적인 방법으로 처리할 수 있습니다.

    code
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            println("job: I'm running finally")
        }
    }
    delay(1300L) // 약간 delay
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // job 취소 후, 완료될 때까지 대기
    println("main: Now I can quit.")
    result
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    job: I'm running finally
    main: Now I can quit.

    try { } finally { } 표현식과 코틀린의 use 함수는 코루틴이 취소되었을 때 정상적으로 마무리 작업을 수행합니다.


    취소 불가능한 블럭 실행

    이전 예제에서 finally 블럭에서 suspending function을 사용하려고 하면 코드를 실행하는 코루틴이 취소되어 Cancellation Exception이 발생합니다.

    드물게 종료된 코루틴에서 suspend의 기능을 사용해야 하는 경우, withContext함수와 NonCancellable 컨텍스트를 사용하여 withContext(NonCancellable) { }에서 해당 코드를 래핑할 수 있습니다.

     

    code
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // 약간 delay
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // job 취소 후, 완료될 때까지 대기
    println("main: Now I can quit.")
    result
    job: I'm sleeping 0 ...
    job: I'm sleeping 1 ...
    job: I'm sleeping 2 ...
    main: I'm tired of waiting!
    job: I'm running finally
    job: And I've just delayed for 1 sec because I'm non-cancellable
    main: Now I can quit.

    타임아웃

    코루틴 실행을 취소하는 가장 명백한 이유는 실행시간이 제한된 시간을 어느정도 초과하였기 때문입니다.

    code
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    result
    I'm sleeping 0 ...
    I'm sleeping 1 ...
    I'm sleeping 2 ...
    Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

    withTimeout에서 던져진 TimeoutCancellationException은 CancellationException의 하위 클래스입니다. 이전 콘솔에서 이 예외의 stack trace가 출력되는 것을 볼 수 없습니다. 취소된 코루틴 내에서의 CancellationException은 코루틴 완료에 대한 정상적인 전제로 간주되기 때문입니다.

     

    code
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // 해당 결과값을 생성하기 전에 취소됩니다
    }
    println("Result is $result")
    result

     

    I'm sleeping 0 ...
    I'm sleeping 1 ...
    I'm sleeping 2 ...
    Result is null

     

    withTimeoutOrNull 함수는 withTimeout과 비슷하나 Timeout일 경우 예외를 던지는 대신 null을 리턴합니다.


    비동기 타임아웃과 리소스

    withTimeOut에서 timeout 이벤트는 해당 블럭에서 실행되는 코드에 대해 비동기식으로 처리되며, 타임아웃 블럭에서 리턴되기 직전까지도 언제든지 발생할 수 있습니다.

    code
    var acquired = 0
    
    class Resource {
        init { acquired++ } // 리소스 획득
        fun close() { acquired-- } // 리소스 해제
    }
    
    fun main() {
        runBlocking {
            repeat(100_000) { // 10만개의 코루틴 실행
                launch { 
                    val resource = withTimeout(60) { // 60ms의 시간제한
                        delay(50) // 50ms간 Delay
                        Resource() // 리소스 획득 후, withTimeout 블럭에서 리턴    
                    }
                    resource.close() // 리소스 해제
                }
            }
        }
        // runBlocking 외부, 모든 코루틴 완료(complete)
        println(acquired) // 여전히 획득(acquired)상태인 리소스의 개수 출력
    }
    result

    0~100_000 중 하나의 수가 랜덤으로 출력됩니다.

    리소스가 유출되는 문제를 해결하기 위해서는 아래와 같은 방법으로 사용할 수 있습니다.

    code
    runBlocking {
        repeat(100_000) { // 코루틴 10만개 실행
            launch { 
                var resource: Resource? = null // 아직 리소스 미획득
                try {
                    withTimeout(60) { // 60 ms의 시간제약
                        delay(50) // 50 ms간 딜레이
                        resource = Resource() // 리소스 획득 시, 변수에 리소스 저장    
                    }
                    // 획득한 리소스로 다른 작업 가능
                } finally {  
                    resource?.close() // 획득했던 리소스 해제
                }
            }
        }
    }
    // runBlocking 외부, 모든 코루틴 완료(complete)
    println(acquired) // 여전히 획득(acquired)상태인 리소스의 개수 출력
    result
    0

    매번 0이 출력됩니다. 리소스가 유출되지 않습니다.

    반응형

    댓글

Designed by Me.