반갑습니다!

[Kotlin] Cancellation and Timeouts 본문

Kotlin

[Kotlin] Cancellation and Timeouts

김덜덜이 2020. 7. 4. 19:46

Cancelling coroutine execution

이번엔 코루틴을 멈추는 방법을 알아보자. 이전의 예제들을 보면서 launch 를 통해 Job객체를 반환받을 수 있음을 알았다. 코루틴을 멈추는 방법은 반환받은 Job 객체에 구현된 cancel() 함수를 실행하는 것이다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // 1300ms동안 기다리기
    println("main: I'm tired of waiting!")
    job.cancel() //job 취소
    job.join() // job이 종료될 때까지 대기
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

Cancellation is cooperative

위의 예제에서는 설명하지 않았지만 사실, 코루틴을 취소하기 위해서는 코루틴 안에 suspend 함수가 호출되어야 한다. 그렇기 때문에 suspend 함수의 호출 없이 단순 연산만을 수행하는 코루틴은 중지시킬 수 없다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

cancelAndJoin() 함수는 그저 cancel()join() 을 순차적으로 실행하는 함수이다. 위 예제에서 코루틴은 멈춰지지않고 모든 작업을 완료한 뒤 종료되었다. 코루틴 안에 suspend 함수가 호출되지 않았기 때문이다. 이번엔 suspend 함수인 delay() 를 추가해보겠다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            delay(1L) // suspend 함수인 delay 추가
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

suspend 함수를 추가함으로서 정상적으로 중단되는 것을 볼 수 있다. 그렇지만 코틀린을 중단하기 위해서 delay() 를 넣는 것은 적절하지 않아보인다. 이를 위해서 존재하는 함수가 yield() 이다. 위의 예제에서 delay()yield() 로 변경하면 정상적으로 동작함을 알 수 있을 것이다. 내부적으로 원리를 조금 설명하자면 suspend 함수는 실행하기 전에 cancel이 되었는지를 확인하고 cancel 되었다면 JobCancellationException 을 발생시켜 코루틴을 종료시킨다.

Making computation code cancellable

그렇다면 suspend 함수가 없다면 코루틴은 중단할 수 없는 것인가? 답은 '아니오' 이다. 그렇다면 어떤 방식으로 할 수 있을지 다음 예제를 보면서 알아보자.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 코루틴 내부에 있는 isActive의 상태 확인
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

코루틴 내부에는 ìsActive라는 Boolean 형 변수가 있다. 이 변수는 cancel을 하면 false로 변하기 때문에 이 값을 사용해서 코루틴을 제어할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")    
}

/* 실행결과
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 가 처음 방식과 다른 점은 exception을 발생시키지 않는다는 점이다.

Closing resources with finally

suspend 를 사용하면 코루틴을 멈출 때 예외를 발생시키기 때문에 이를 이용해서 리소스의 해제를 쉽게 할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    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 a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

위 예제를 보면 코틀린을 멈추자 예외가 발생해 finally 블록이 실행되었음을 알 수 있다. 이를 이용해 코틀린 중단 시 finally 블록 안에서 리소스를 반납해주면 된다.

Run non-cancellable block

간혹가다 중지된 코루틴에서 지연을 해야할 때가 있을 수 있다. 이러한 경우 withContext()함수와 NonCancellable context를 사용하면 된다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    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)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")    
}

/* 실행결과
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.
*/

Timeout

이번엔 코루틴 자체적으로 Timeout을 설정해서 종료하는 방식이다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
}

/* 실행결과
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 ...
...
*/

이 예제를 실행해보면 TimeoutCancellationException이 발생하면서 종료되었다. 예외처리 코드를 별도로 추가해도 되지만 예외처리 없이 좀 더 간단하게 코드를 작성하면 다음과 같이 작성할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done" // 코루틴이 완전히 실행되면 "Done"을 반환
    }
    println("Result is $result")
}

/* 실행결과
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
*/

withTimeoutOrNull()함수를 사용하면 timeout이 발생한 경우 예외를 발생시키는 것이 아니라 null값을 리턴한다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Principle of Coroutine  (0) 2020.07.05
[Kotlin] Composing Suspending Functions  (0) 2020.07.05
[Kotlin] Coroutine Basics  (0) 2020.07.04
[Kotlin] Coroutine Guide  (0) 2020.07.04
[Kotlin] 반복문  (0) 2020.06.15