본문 바로가기
개발 지식/Kotlin

[Coroutine] Coroutine Cancellation And Timeout

by 에르주 2022. 8. 29.
반응형

지난번 장에서는 코루틴은 Builder로 생성해보는 것까지 진행하였다.

이번장에서는 코루틴 종료하는 2가지 방법 

1) Cacellation

2) Timeout

 

에 대해 정리해보고자 한다.

 

Cacellation 즉 코루틴이 종료 되기 위해서는 다음과 같은 세가지 조건을 만족해야 한다.

 

  • 코루틴이 취소 되기 위해서는 다음과 같이 협조적이어야 한다.
  • 코루틴 코드가 cancellable 해야 한다
  • suspend 함수는 취소 가능하다

이에 다음과 같은 예시를 코드로 작성해보고 테스트 해보자.

 

  1. join 객체의 cacel 함수 호출
  2. join 객체의 cacel 함수 호출, But suspend 함수가 없다면?
  3. suspend 함수가 아닌 상태체크로 Cancel
  4. finally로 코루틴 종료 
  5. NonCancellable로 종료된 코루틴 다시 호출하기
  6. Timeout으로 코루틴 종료 시키기
  7. Timeout으로 코루틴 종료시키며 원하는 결과값 리턴시키기

 

 

예시 코드와 함께 정리해보자.

 

1) join 객체의 cacel 함수 호출

fun main() = runBlocking { // main Thread Blocking
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L) // suspend function
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancel() // job 객체 취소 cancels the job -> 1000번까지 모두 실행
    job.join()
    println("main: Now I can quit.")
}

종료

 

 

    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L) // suspend function
        }
    }

 

위의 코루틴에서 delay(500L) suspend 함수를 적용했으므로 1000번 실행되는 것이 아닌 중간에 취소 되는 것을 확인 할 수 있다.

 

 

2) join 객체의 cacel 함수 호출, But suspend 함수가 없다면?

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()

    // launch안에 suspend 함수가 없다
    val job = launch(Dispatchers.Default) {

        try {

            var nextPrintTime = startTime
            var i = 0
            while (i < 5) {
                if (System.currentTimeMillis() >= nextPrintTime) {     
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        } catch (e: Exception) {
            kotlin.io.println("Exception [$e]")
        }
    }

    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancel 후 join, cacel은 코루틴을 강제로 Exception 발생시켜 종료되게 만든다.
    println("main: Now I can quit.")

}

4개 모두 실행

코루틴 내에서 suspend가 없는 조건에서는
main 함수 내에서 delay는 1.3초이고 코루틴은 0.5초 이므로 1번 예제와 같이 i가 2까지만 실행되었어야 했는데 i < 5 범위 모두가 실행된 것을 볼 수 있다.

 

코루틴에 suspend 함수를 넣어보자

  • delay(1L) : delay suspend 함수 
  • yield() : 코루틴 취소를 위한 suspend 함수 -> 그저 종료 체크를 위한 함수라고 생각하면 된다.
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()

    // launch안에 suspend 함수가 없다
    val job = launch(Dispatchers.Default) {

        try {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    delay(1L) // suspend 함수인 delay를 추가하면 일시정지된다.
                    yield() // 종료 체크 suspend 함수
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        } catch (e: Exception) {
            kotlin.io.println("Exception [$e]")
        }
    }

    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancel 후 join, cacel은 코루틴을 강제로 Exception 발생시켜 종료되게 만든다.
    println("main: Now I can quit.")

}

 

코루틴 종료 확인

추가적으로 

try, catch를 통해 cancel 되었을 때에는 JobCancellationException이 발생하는 것을 확인 할 수 있다.

 

즉, 코루틴이 Cancel 될 때에는 JobCancellationException 이 발생한다. 강제로 Exception 발생 시켜 종료 되게 만든다. 

 

3) suspend 함수가 아닌 상태체크로 Cancel

 

suspend 함수를 적용하지 않아도 코루틴 내부의 상태체크를 통해 비정상이면 코루틴을 종료 시킬 수 있다.

fun main()  = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        try {
            var nextPrintTime = startTime
            var i = 0
            kotlin.io.println("isActive $isActive ... ")
            // isActive : 코루틴의 확장 Property 코루틴의 job 종료 여부 체크
            while (isActive) { // 명시적으로 상태 체크, exception을 던지지 않고 코루틴 스스로 상태값으로 종료한다.
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        catch (e :Exception) {
            kotlin.io.println("Exception [$e]")
        }
        kotlin.io.println("isActive $isActive ... ")
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancel 후 join, cacel은 코루틴을 강제로 Exception 발생시켜 종료되게 만든다.
    println("main: Now I can quit.")
}

 

while 조건으로  isActive 즉 상태체크를 하여 코루틴을 종료시킨다.

 

isActive로 종료

 

  1. fun main 함수 delay 1.3초 발생
  2. 코루틴 생성, 최초 isActive = true
  3. 0.5초 마다 console print
  4. "job: I'm sleeping 2" 까지 console print 
  5. "main: I'm tired of waiting!" console print
  6. isActive= false로 변경
  7. isActive = false 이므로 코루틴 종료

순서로 진행된다.

 

 

4) finally로 코루틴 종료 

exception 방지를 위해 try catch 문을 활용하든 코루틴 스코프 핸들러 역할을 하는 try finally가 있다.

밑의 로직에서 cancelAndJoin 함수로 코루틴은 exception을 발생시키는데  finally는 코루틴이 exception 발생 즉 종료시 호출된다.

 

fun main() = runBlocking {

    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L) // suspend 함수 일시 중지 및 재개 되면서 exception 던짐
            }
        } finally {
            // exception 던질 때 리소스 해제 시 finally를 부른다.
            println("job: I'm running finally")
        }
    }

    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancel 후 join, cacel은 코루틴을 강제로 Exception 발생시켜 종료되게 만든다.
    println("main: Now I can quit.")
}

 

실행

 

5) NonCancellable로 종료된 코루틴 다시 호출하기

 

드문 케이스 중 하나로 종료된 코루틴을 다시 호출해야 하는 경우가 있다.

이는 withContext(NonCancellable)을 활용한다.

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L) // cancel -> exception 던짐
            }
        } finally { // exception 받았다!
            withContext(NonCancellable) {
                println("job: I'm running finally")
                delay(1000L)
                println("job: And I've just deleayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancel 후 join, cacel은 코루틴을 강제로 Exception 발생시켜 종료되게 만든다.
    println("main: Now I can quit.")
}

noncancellable 실행

 

6) Timeout으로 코루틴 종료 시키기

 

위에 예제들은 suspend 또는 isActive 또는 finally로 코루틴을 종료를 할 수 있었다.

코루틴 Timeout 설정을 통해 추가로 종료 시킬 수 있다.

 

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

 

timeout

위에는 main 함수의 runBlocking에서 코루틴이 종료된 것이기 때문에 서비스 자체가 종료되는 것을 확인 할 수 있다.

 

 

7) Timeout으로 코루틴 종료시키며 원하는 결과값 리턴시키기

 

withTimeoutOrNull 함수를 통해 코루틴 종료시 Null값으로 반환시킬 수 있다.

fun main() = runBlocking {
    val result = withTimeoutOrNull(1300L) {
        repeat(1000) { i->
            println("I'm sleeping $i ...")
            delay(500L)
        }
        "Done"
    }
    println("Result is $result")
}

 

Exception오류가 나는 것이 아닌 Null값으로 종료 되는 것을 확인 할 수 있다.

 

끝.

반응형

댓글