Coroutine - 3 타임아웃, 취소

 

 

Coroutine - 2 기본 사용법

Coroutine 개념 코루틴이란? 코루틴은 Kotlin 을 사용해 안드로이드 앱을 개발할 때 아마 가장 처음 접하게 되는 키워드 일것이다. 보통 네트워크 통신, 혹은 내부 DB에서 데이터를 불러오거나 저장할

forstudy.tistory.com

 

1. 코루틴 취소하기


장시간 동안 동작하는 어플리케이션의 경우, 백그라운드에서 동작하는 코루틴에 대해 세세한 컨트롤이 필요하다. 만약에 코루틴이 시작한 상태에서 해당 코루틴의 결과를 받을 웹 페이지, 혹은 어플리케이션이 종료된다면 코루틴의 결과는 필요 없어지고, 취소해야 한다. 

 

만약 더 이상 코루틴의 결과가 필요하지 않는데도 계속 실행한다면, 메모리와 성능을 낭비하고 배터리를 많이 잡아먹게 된다. 

 

이전 글에서 launch 라는 코루틴 빌더는 Job 객체를 반환한다고 했다. 이 Job 객체를 사용해서 스레드를 대기 시킬 수도 있고 아니면 아예 코루틴 자체를 취소할 수도 있다. 

 

다음 코드를 보자

 

fun main() {
    runBlocking {
        val job = launch {
            repeat(1000) {
                println("job: I`m sleeping $it ...")
                delay(500)
            }
        }
        delay(1300)
        println("main: I`m tired of waiting")
        job.cancel()// <-- 코루틴을 취소한다!
        job.join()
        println("main: Now I can quit")
    }
}kotlin

 

launch 코루틴 빌더의 반환값인 Job 객체를 참조 변수에 저장하고, 해당 코루틴을 취소한다. 

 

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.join() 으로 해당 코루틴의 종료를 대기하는 것을 볼 수 있다. job.cancel()을 호출하더라도 코루틴은 바로 종료되지 않는다. cancel 함수 호출 후에 코루틴은 Cancelling 상태에 진입하게 되는데, cancelled 상태에 도달해야만 완전히 종료되었다고 취급할 수 있다.

 

job.cancelAndJoin()

cancelAndJoin 이라는 함수를 호출해 한 줄로 줄일 수도 있다.

 

2. 코루틴 취소는 매우 쉽지만, 주의가 필요하다


모든 코루틴은 취소 가능하다. 코루틴은 항상 취소 여부를 확인하고 (isActive 라는 프로퍼티), 만약 취소 됐을 경우 CancellationException 을 던진다. 그러나 만약 코루틴이 계속해서 연산 중이고 cancellation 여부를 확인할 수 없다면, 코루틴은 취소되지 않는다. 다음과 같은 코드를 보자

 

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
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.")kotlin

 

이전 코드와 똑같이 동작을 하도록 코드를 작성해보았다. 명시적으로 1.3초 뒤에 코루틴을 취소하게 했으므로, "job: I`m sleeping" 이라

는 문장이 세 번만 호출되는 것을 기대할 수 있다. 그러나 우리의 기대완 다르게 이 코드의 결과는 다음과 같다.

 

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

 

그 이유가 무엇일까? 바로 위에서 코루틴을 cancel 하더라도 바로 취소가 되는 것은 아니라고 했다. 내부적으로 취소 여부를 확인하는 과정을 거쳐야 비로소 취소가 되는데, 위 코루틴의 while 문에서 빠져나올 수가 없어 취소 여부를 검사할 수 없다. 

 

3. 계산 작업 중인 코루틴 취소하기


바로 위의 코루틴을 취소할 수 있는 방법은 뭐가 있을까? 코루틴에는 isActive 라는 Cancellation 여부를 나타내는 프로퍼티가 존재한다. 우리는 명시적으로 이 프로퍼티의 상태를 체크함으로써 좀 더 쉽게 코루틴의 취소가 가능하다. 

 

while (i < 5) 코드를 while (isActive) 코드로 변경한 뒤에 코드를 다시 실행해보자

 

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (isActive) { // cancellable computation loop
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
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.")kotlin

이제 while 문은 isActive 라는 프로퍼티를 체크한 뒤, 만약 cancel 상태면 해당 while 문을 빠져나오게 된다. 그러면 비로소 위의 코루틴을 정상적으로 종료할 수 있는것이다.

 

4. finally 로 자원 해제하기


suspend 함수를 취소하면 CancellationException 을 던진다. 이 Exception 은 다른 것들과 마찬가지로 try 문으로 다룰 수 있다. 

 

runBlocking {
    val job = launch(Dispatchers.Default) {
        try {
            for (i in 1..5) {
                println("hihi")
                delay(500)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            println("I`m Done")
        }
    }
    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.")
}

위 코드의 결과는 다음과 같다

 

hihi
hihi
hihi
main: I'm tired of waiting!
I`m Done
main: Now I can quit.kotlin

5.  

6. non-cancellable block 실행


주로 자원을 해제하는 코드는 동기적으로 작동하고, suspendable 하게 작동하지 않는다. 따라서 코루틴을 취소하더라도 정상적으로 finally 에서 자원을 해제할 수 있다. 그러나 드물게 취소된 코루틴에서 suspend 함수를 실행해야 하는 경우가 있다. 일반적인 경우라면 취소된 Coroutine 에서 suspend 함수를 실행하자마자 CancellationException 을 던진다. 

 

그러나 withContext(NonCancellable){}  를 사용하면 취소된 Coroutine 에서 suspend 함수를 실행할 수 있다 

 

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) // 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.")
    }
}kotlin

위 코드의 실행 결과는 다음과 같다.

 

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

 

7. 코루틴에 타임아웃 설정하기 


코루틴을 취소해야 하는 가장 주된 원인은 해당 코루틴이 시간제한을 초과했기 때문일 것이다. 예를 들어 로그인 같은 간단한 네트워크 작업을 수행할 때 네트워크 상황이 좋지 않아 20초 이상 결과를 대기하고 있다면,  5초 가량의 타임아웃을 걸고 에러 메시지를 띄우는 편이 훨씬 나을 것이다. 

 

 물론 launch 로 코루틴의 객체를 직접 얻어와서 타임아웃을 넘어가면 해당 코루틴을 취소하는 방법도 가능하다. 하지만 이미 withTimeout 이라는 같은 동작을 하는 함수가 정의되있다. withTimeout은 파라미터로 시간제한을 넘기고, suspend block 을 같이 넘겨준다. 

 

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

 

withTimeout 도 launch 와 마찬가지로 coroutineBuilder 이지만, suspend 함수이기 때문에 코루틴 스코프안에서 호출되야 한다. 

만약 시간제한이 초과하게 되면 TimeoutCancellationException 을 던지게 된다. 이 Exception 은 try{...}~ catch(e:TimeoutCancellationException) 문으로 처리해줄 수 있다. 

 

withTimeoutOrNull 이라는 함수도 존재하는데, 이 함수는 타임아웃이 초과하더라도 예외를 던지지 않는다. 

 

8.  

'Kotlin' 카테고리의 다른 글

Coroutine - 2 기본 사용법  (0) 2022.01.30
Coroutine 개념  (0) 2022.01.29