ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공식 가이드 문서로 Coroutine 공부하기 - [02. Cancellation and Timeouts]
    개발/Kotlin 2020. 11. 6. 06:57

     

    [ 02. 취소 및 시간 초과 ]

     

     


     

    해당 섹션에서는 코루틴 취소 및 시간 초과에 대해 설명합니다.

     

     

    # 코루틴 실행 취소하기


    장시간 구동되는 애플리케이션에서는 백그라운드 코루틴에 대한 세밀한 제어가 필요할 수 있습니다. 예를 들어, 사용자가 코루틴을 시작시킨 페이지를 닫았을 수 있으며 이제 그 결과를 필요로 하지 않고 해당 작업을 취소할 수 있습니다. 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에는 canceljoin의 호출이 결합시킨 cancelAndJoin 확장 함수도 존재합니다.

     

     

     

     

    # 취소는 협조적입니다


    코루틴 취소는 협조적입니다. 코루틴 코드는 취소 가능하도록 협조해야 합니다. 모든 kotlinx.coroutines suspending function은 취소가능합니다. 이 들은 코루틴의 취소여부를 확인하고, 취소 되었을 때 CancellationException 던집니다. 그러나 코루틴이 계산(computation)작업을 하고있으며 취소여부를 확인하지 않으면  다음 예제 코드에서 알 수 있듯, 취소할  없습니다.

     

    - 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.

     

    코드를 실행하여, 취소 후에도 job이 자동으로 완료될 때까지 계속하여 "I'm sleeping"이 5회 출력되는 지 확인합니다.

     

     

     

     

    # 계산(computation) 코드 취소 가능하도록 만들기


    computation 코드를 취소가능하도록 만드는 두 가지 접근법입니다. 첫 번째는 취소여부를 확인하는 suspending function을 주기적으로 호출하는 것입니다. 그 목적에 적합한 yield함수가 존재합니다. 다른 한 방법은 명시적으로 취소여부를 확인하는입니다. 후자의 접근 방식대로 시도해보겠습니다.

    이전 예제의 while (i < 5)while (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.

     

    보다시피 이제 루프가 취소되었습니다. isActive CoroutineScope 객체를 통해 코루틴 내에서 실행가능한 확장 프로퍼티입니다.

     

     

     

     

    # finally를 사용하여 리소스 닫기 (Close)


    취소가능한 suspending function은 취소 시, CancellationException를 던지며 이는 일반적인 방법으로 처리할 수 있습니다. 예를 들어, try {...} finally {...} 표현식과 코틀린의 use 함수는 코루틴이  취소되었을 때 정상적으로 마무리 작업을 수행합니다.

     

    - 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.

     

    joincancelAndJoin는 마무리 작업이 완료될 때까지 기다리므로, 예제 코드는 위와 같이 출력합니다.

     

     

     

     

    # 취소 불가능한 블럭 실행


    이전 예제의 finally 블럭에서 suspending function를 사용하려고하면 코드를 실행하는 코루틴이 취소되었으므로  CancellationException 발생합니다. 모든 정상적인 닫기 작업(파일 닫기, job 취소하기 또는 통신채널 닫기)은 일반적으로 차단되지 않으며(non-blocking) 어떠한 suspending function을 포함하지 않으므로, 일반적으로 해당 작업은 문제되지 않습니다. 그러나 드물게 종료된 코루틴에서 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.

     

     

     

     

    # 타임아웃


    코루틴 실행을 취소하는 가장 명백한 실질적인 이유는 실행시간이 제한된 시간을 어느정도 초과하였기 때문입니다. 해당 Job에 대한 참조를 수동으로 추적하고 delay 후 추적된 코루틴을 취소하기 위해 별도의 코루틴을 실행할 수 있지만, withTimeout 함수를 사용할 수 있습니다. 다음 예제코드를 확인하겠습니다.

     

    - 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에서 던져진 TimeoutCancellationExceptionCancellationException 하위 클래스입니다. 우리는 이전에 콘솔에서 이 exception의 stack trace가 출력되는 것을 본 적 없습니다. 그 이유는 취소된 코루틴 내에서의 CancellationException 은 코루틴 완료에 대한 정상적인 전제으로 간주되기 때문입니다. 그러나 해당 예제코드에서는  main 함수 바로 내부에서 withTimeout 을사용하였습니다.

     

    취소는 그저 예외일뿐이므로 모든 리소스는 일반적인 방식으로 닫힙니다. Timeout에 대해 특별히 추가 작업을 해야하거나 withTimeoutOrNull 함수를 사용해야하는 경우, try {...} catch (e: TimeoutCancellationException) {...} 블럭에서 Timeout 코드를 래핑할 수 있습니다. withTimeoutOrNull 함수는 withTimeout 비슷하나 Timeout일 경우 예외를 던지는 대신 null을 리턴합니다.

     

    - 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

     

    해당 코드를 실행할 때, 더 이상 예외를 확인할 수 없습니다.

     

     

     

     

    # 비동기 타임아웃과 리소스


    withTimeout에서의 timeout 이벤트는 해당 블럭에서 실행되는 코드에 대해 비동기식으로 처리되며, 타임아웃 블럭에서 리턴되기 직전까지도 언제든지 발생할 수 있습니다. 코드블럭 외부에서 닫거나 해제해야하는 일부 리소스를 블럭 내부에서 열거나 획득하는 경우, 이를 염두에 두어야합니다.

     

    예를 들어, 여기서는 acquired 카운터 증가 및 close 함수를 통한 acquired 카운터 감소 기능을 통해, 생성된 리소스의 개수를 추적하는 Resource 클래스를 사용하여 닫기(close) 가능한 리소스를 모방합니다. 작은 시간 제약의 많은 코루틴을 실행시키고, 각 코루틴에서 약간의 delay 후에 withTimeout 블럭 내부에서 이 리소스를 획득하고 외부에서 해제해보십시오.

     

    - 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 :

    613

    * 직접 실행해본 결과, 0 ~ 100_000 중 하나의 수가 매번 랜덤으로 출력되었습니다.

     

    위의 코드를 실행해보았을 때, 매번 0(zero)가 출력되지는 않으나, 컴퓨터의 타이밍에 따라 다를 수 있지만 실제로 0이 아닌 값을 보려면 해당 예제에서 시간 제약을 조정해야 할 수도 있습니다.

     

     


    여기에서 100K개의 코루틴에서 acquired 카운터를 증가 및 감소시키는 것은 항상 동일한 메인 스레드에서 발생하므로 완벽히 안전합니다. 이에 대한 자세한 내용은 다음 장에서 
    coroutine context에 대해 설명할 예정입니다.

     

     

    이러한 문제를 해결하려면 withTimeout 블럭에서 리턴시킨것과는 반대로, 리소스에 대한 참조를 변수(variable)에 저장하는 방법을 사용할 수 있습니다.

     

    - 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(zero)를 출력합니다. 리소스가 유출되지 않습니다.

     

     

     

     


     

     

    Cancellation and Timeouts - Kotlin Programming Language

     

    kotlinlang.org

     

     

    댓글

Designed by Tistory.