코루틴 에서의 작업 취소
코루틴의 완벽한 제어를 위해서는 작업을 기다리고, 완료된 작업의 결과를 반환 받아서 처리하는것 뿐만아니라 작업의 취소 까지도 처리할수 있어야 합니다.
제어에 사용 되는 Job 클래스 와 Deferred 클래스 에는 코루틴 블록의 작업을 취소하기 위한 cancel() 함수가 존재합니다.
다음은 500 밀리초 간격으로 특정 문자열을 1000회 출력하는 코루틴 블록입니다. 이 블록을 시작한 이후 1300 밀리초가 지나면 코루틴을 취소합니다.
fun cancellingCoroutineExecution() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancel()
job.join()
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.
3번의 코루틴 블록의 수행 이후 취소 됐고, 원하는 대로 종료되었습니다.
다음은 위의 예시와 동일한 일을 수행하는 코루틴 블록 입니다.
단, 코틀린 에서 제공하는 repeat() 함수 와 delay() 함수를 사용하지 않고 while 문과 시스템 시간을 이용해 직접 500 밀리초 간격으로 작업을 수행하도록 작성해 보았습니다.
fun cancellationIsCooperative() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 10) {
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!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
job: I'm sleeping 5 ...
job: I'm sleeping 6 ...
job: I'm sleeping 7 ...
job: I'm sleeping 8 ...
job: I'm sleeping 9 ...
main: Now I can quit.
1300 밀리초 후 취소를 시도했지만, 취소되지 않고 10회를 모두 수행한 후 종료 되었습니다.
취소는 어떻게 작동하는가 ?
Job이 취소되면 Cancelling 상태로 바뀝니다. 하지만, Job의 작업이 완료된 후에야 Cancelled 상태로 이동이 되어 실제 취소가 되게 됩니다. (또는 취소 시점으로 부터 첫 번째 중단 지점을 만났을 경우 Cancelled 상태가 됩니다.)
즉, job.cancel()이 호출된다고 해서 코루틴 작업이 중단 되는 것이 아닙니다.
Job은 여러 상태를 거칠 수 있습니다 : New, Active, Completing, Completed, Cancelling, Cancelled
상태 자체에 접근할 수는 없지만 Job의 속성에 접근할수는 있습니다: isActive, isCancelled, isCompleted
코루틴 취소가 가능하도록 협조 합시다.
코루틴의 코드는 취소가 가능하도록 협력해야 하며, 주기적으로 취소여부를 확인해여야 합니다.
즉, 우리가 작성하는 코루틴 코드도 취소가 올바로 동작할수 있도록 노력해야 합니다.
kotlinx.coroutines에 정의되어 있는 모든 Suspending Function 들은 Cancellable(취소 가능한) 형태이며, 이들은 코루틴 동작의 취소 요청이 들어왔을 때 이를 확인하고 CancellationException 오류를 Throwing합니다. 이들을 활용하여 우리는 코루틴 취소가 가능하도록 협조할 수 있습니다.
하지만 이는 강제가 아니기때문에 신경쓰지 않는다면 두번째 예제와 같이 취소 할수 없는 코드가 작성될수도 있습니다.
취소가 가능한 코루틴 블록 만들기
코루틴 블록이 취소 가능하게 하기 위한 방법은 다음과 같습니다.
1. kotlinx.coroutines 패키지 함수 사용
kotlinx.coroutines 패키지의 모든 기능은 취소 가능 하도록 작성되어 있습니다.
delay() 함수도 kotlinx.coroutines 패키지 에 속해 있기때문에 이를 사용한 첫번째 예제는 delay() 함수 에서 취소가 동작하고 코루틴 블록을 취소 할수가 있습니다.
또는 kotlinx.coroutines 패키지에 포함되어 있는 yield() 함수를 사용할수도 있습니다.
yield() 함수는 해당 위치에서 코루틴을 일시중단합니다. 이때 코루틴의 취소 여부를 확인하게 되므로 yield() 함수를 호출한 해당 위치에서 코루틴 취소가 가능해 집니다.
fun makingComputationCodeCancellableUsingYield() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 20) {
yield()
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.")
}
kotlinx.coroutines 패키지의 함수들을 코루틴 블록안에 적극 활용하면, 해당 블록은 취소 가능해 집니다.
(또는 suspend 함수)
2. CoroutineScope 의 확장 프로퍼티 isActive 로 확인
CoroutineScope 에는 Boolean 값의 확장 프로퍼티 isActive 를 가지고 있습니다. 이 값을 통해 이 코루틴 블록이 아직 취소 되지 않았는지 상태를 확인할수 있습니다.
isActive 프로퍼티 는 다음과 같이 사용할수 있습니다.
fun makingComputationCodeCancellable() = 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.")
}
이와 같이 명시적으로 코루틴 의 취소 여부를 확인하여 코루틴을 설계할수도 있습니다.
코루틴 작업 취소 시 마무리 작업 하기
네트워크 나 파일을 다루는 경우에 우리는 모든 작업이 정상 종료되거나, 혹은 비정상 종료될때 사용한 리소스를 정리할수 있도록 프로그래밍 해야 합니다.
코루틴 역시 취소 시에 마무리 작업을 할수 있습니다.
try-finally 문으로 마무리 작업 하기
try-finally 문으로 코루틴 블록을 감싸면, 작업을 종료할때 finally 의 블록 코드가 실행 됩니다.
fun closingResourcesWithFinally() = 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)
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
main: Now I can quit.
finally 문 에서 기다리기
만약 finally 에서 어떤 리소스의 사용이 마무리 되기를 대기해야 한다고 가정해봅시다.
그렇다면 finally 안에서 delay() 함수를 이용해서 일정 시간 대기 후에 작업을 이어가면 된다고 생각할수 있습니다.
하지만 delay() 함수 역시 코루틴 블록의 취소에 영향을 받기 때문에 finally 안에서 사용할수 없습니다.
같은 이유로 kotlinx.coroutines 패키지의 함수들은 finally 문 에서 사용하면 CancellationException이 발생합니다.
이러한 동작이 반드시 필요한 경우 withContext() 함수를 사용할수 있습니다.
fun runNonCancellableBlock() = 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.")
}
withContext() 함수와 NonCancellable 를 사용하면, withContext() 함수 내의 코루틴 블록 은 취소되지 않습니다.
다음은 해당 코드의 수행 결과 입니다.
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 으로 동작하는 코루틴 블록을 작성하는 방법입니다.
다양한 이유로 코루틴 의 작업을 취소 할 필요가 있습니다. 그중 가장 빈번히 발생하는 이유는 유효한 처리 시간을 초과 하여 더이상의 작업이 무의미 하기 때문입니다.
withTimeout() 함수 사용
코루틴에는 Timeout 동작을 간단히 처리할수 있는 withTimeout() 함수를 제공하여 kotlinx.coroutines 패키지에 포함되어 있습니다.
withTimeout() 함수는 첫번째 인자로 작업을 수행할 시간, 두번째 인자로 수행할 블록 함수를 받습니다.
다음은 1300 밀리초 이후 작업을 취소 합니다.
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
작업 취소 시 TimeoutCancellationException 이 발생하기 때문에 작업 취소 보다는 애플리케이션이 강제 중지 된다고 표현하는게 맞습니다.
try-catch 를 이용해 TimeoutCancellationException 을 처리할수도 있지만, 코틀린에서 제공하는 withTimeoutOrNull() 함수를 이용할수도 있습니다.
withTimeoutOrNull() 함수 사용
withTimeoutOrNull() 함수도 kotlinx.coroutines 패키지에 포함되어 제공됩니다.
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done"
}
println("Result is $result")
시간내 정상 종료 시 값을 반환할수도 있고, 만약 시간내 처리되지 못한다면 withTimeout() 함수 와는 다르게 TimeoutCancellationException 이 발생하지 않고, 값으로 null 이 반환 됩니다.
마침말
코루틴으로 작업을 설계할때 어떻게 처리 할것인가 만큼 어떻게 작업을 취소 할것인가 역시 중요합니다. 항상 코루틴 블록이 작업 취소 처리 에 협조적일수 있도록 고려해야 합니다.
이로서 코루틴 코루틴 기초 의 모든 포스트를 마칩니다.
감사합니다.
참고자료
코틀린 코루틴 작업 취소
코틀린 코루틴 3부작 중 기초, 제어 에 이은 마지막 포스트, 코루틴 작업 취소 입니다.
medium.com
https://velog.io/@haero_kim/Kotlin-Coroutine-%EC%B7%A8%EC%86%8C%ED%95%98%EA%B8%B0
[Kotlin] Coroutine 취소하기
마! 실행할 줄 알믄 취소할 줄도 알아야제!
velog.io
https://android-devpia.tistory.com/10
코틀린 코루틴에서의 취소 및 예외 처리 #1 - CoroutineScope, Job, CoroutineContext
이 포스팅은 아래 게시글을 번역 및 일부 수정하여 작성하였습니다. https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21 이 일련의 포스팅은 코루틴의 취소와 예외에 대하여 자세히 설
android-devpia.tistory.com
'Android > Coroutine' 카테고리의 다른 글
[Android] 코루틴(Coroutine) 내부적으로 어떻게 동작할까? (0) | 2025.01.24 |
---|---|
[Android] 콜백을 코루틴(Coroutine)으로 바꿔보자(SuspendCoroutine/SuspendCancellableCoroutine) (2) | 2024.11.16 |
[Android] 코루틴(Coroutine) 예외 처리 방법 (1) | 2024.11.09 |
[Android] 코루틴(Coroutine)의 구조적 동시성(Structured Concurrency) (0) | 2024.11.09 |
[Android] 코루틴(Coroutine) Builder에 대한 고찰 - Deep Dive (0) | 2024.11.09 |