Cancellation and timeouts

취소

오래 실행되는 어플리케이션에서는 백그라운드 코루틴에 대해 세밀한 제어가 필요할 수 있다.
예를 들어, 사용자가 코루틴을 시작한 페이지를 닫았을 때 그 결과는 더 이상 필요하지 않으며 작업을 취소할 수 있다.
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.
Kotlin
복사
launch 함수는 실행 중인 코루틴을 취소하는데 사용할 수 있는 Job을 반환한다.
main이 job.cancel을 호출하자마자 다른 코루틴의 출력이 더 이상 보이지 않으며 이는 취소되었기 때문이다.
cancelAndJoin 이라는 Job의 확장 함수는 cancel과 join 호출을 결합한다.

취소는 협조적이다.

val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (i < 5) { // CPU 낭비 루프 // 2초마다 메시지를 출력 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.")
Kotlin
복사
코루틴 코드는 취소 가능하도록 협조해야 한다.
kotlinx.coroutines의 모든 일시 중단 함수는 취소 가능하며 취소되면 CancellationException을 발생시킨다.
그러나 코루틴이 계산 작업을 수행하며 취소를 확인하지 않으면 취소할 수 없다.
val job = launch(Dispatchers.Default) { repeat(5) { i -> try { // 2초마다 메시지를 출력 println("job: I'm sleeping $i ...") delay(500) } catch (e: Exception) { // 예외를 기록 println(e) } } } delay(1300L) // 잠깐 지연 println("main: I'm tired of waiting!") job.cancelAndJoin() // 작업을 취소하고 완료를 기다림 println("main: Now I can quit.")
Kotlin
복사
CancellationException을 try-catch로 잡고 재발생시키지 않을 때도 취소가 되지 않는다.
예외를 잡는 것은 안티패턴이며 runCatching 함수와 같이 CancellationException을 재발생시키지 않는 경우에도 이 문제가 발생할 수 있다.

계산 코드를 취소 가능하게 만들기

계산 코드를 취소 가능하게 만들기 위한 두 가지 방법이 있다.
1.
주기적으로 일시 중단 함수를 호출하여 취소를 확인하는 것 (yield 함수)
2.
취소 상태를 명시적으로 확인하는 것
val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (isActive) { // 취소 가능한 계산 루프 // 2초마다 메시지를 출력 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.")
Kotlin
복사
isActive는 코루틴의 CoroutineScope 객체 내에서 사용할 수 있는 확장 속성이다.

finally로 자원 닫기

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.
Kotlin
복사
취소 가능한 일시 중단 함수는 취소 시 CancellationException을 발생시키며 이는 일방적인 방식으로 처리될 수 있다.
예를 들어, try-catch-finally 표현식 및 use 함수는 코루틴이 취소될 때 finally 작업을 정상적으로 수행한다.

취소할 수 없는 블록 실행

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.")
Kotlin
복사
finally 블록에서 일시 중단 함수를 사용하려고 하면 CancellationException이 발생한다.
모든 순조로운 닫기 작업들(파일 닫기, job 취소, 채널 닫기 등)은 non-blocking이고 suspend 함수를 포함하지 않기 때문에 일반적으로 문제가 되지 않는다.
그러나 취소된 코루틴에서 일시 중단이 필요한 드문 경우 withContext(NonCancellable) { … } 로 코드를 감쌀 수 있다.

타임아웃

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
Kotlin
복사
코루틴의 실행을 취소하는 가장 명백한 실용적인 이유는 실행 시간이 특정 타임아웃을 초과했기 때문이다.
수동으로 해당 Job에 대한 참조를 추적하고 별도의 코루틴을 실행하여 지연 후에 추적된 코루틴을 취소할 수 있지만 이 작업을 쉽게 수행할 수 있는 withTimeout 함수가 있다.
withTimeout에서 발생하는 TimeoutCancellationException은 CancellationException의 하위 클래스이다.
이전에는 콘솔에 stack trace가 출력되지 않았던 이유는 취소된 코루틴 내부에서 CancellationException이 코루틴 완료의 정상적인 이유로 간주되기 때문이다.
취소는 단지 예외이기 때문에 모든 자원은 일반적인 방식으로 닫힌다.
val result = withTimeoutOrNull(1300L) { repeat(1000) { i -> println("I'm sleeping $i ...") delay(500L) } "Done" // 이 결과를 생성하기 전에 취소됩니다 } println("Result is $result") // 출력: // I'm sleeping 0 ... // I'm sleeping 1 ... // I'm sleeping 2 ... // Result is null
Kotlin
복사
타임아웃에 대해 특별히 추가 작업이 필요하다면 코드 블록을 try-catch 블록으로 감싸거나 타임아웃 시 예외를 발생시키는 대신 null을 반환하는 withTimeoutOrNull 함수를 사용할 수 있다.

비동기 타임아웃과 자원

withTimeout의 타임아웃 이벤트는 블록 내에서 실행되는 코드와 비동기적이며 블록 내부에서 리턴하기 직전에 발생할 수 있다.
블록 내부에서 자원을 열거나 획득할 경우 블록 외부에서 닫거나 해제해야 할 필요가 있음을 유념하라.
var acquired = 0 class Resource { init { acquired++ } // 자원을 획득합니다 fun close() { acquired-- } // 자원을 해제합니다 } fun main() { runBlocking { repeat(10_000) { // 10K 코루틴을 실행합니다 launch { val resource = withTimeout(60) { // 60 ms의 타임아웃 delay(50) // 50 ms 지연 Resource() // 자원을 획득하고 withTimeout 블록에서 반환합니다 } resource.close() // 자원을 해제합니다 } } } // runBlocking 외부에서 모든 코루틴이 완료되었습니다 println(acquired) // 여전히 획득된 자원의 수를 출력합니다 }
Kotlin
복사
위 코드를 실행하면 항상 0이 출력되지 않을 수 있다.
runBlocking { repeat(10_000) { // 10K 코루틴을 실행합니다 launch { var resource: Resource? = null // 아직 획득하지 않았습니다 try { withTimeout(60) { // 60 ms의 타임아웃 delay(50) // 50 ms 지연 resource = Resource() // 획득된 경우 변수를 저장합니다 } // 여기서 자원으로 무언가를 할 수 있습니다 } finally { resource?.close() // 획득된 경우 자원을 해제합니다 } } } } // runBlocking 외부에서 모든 코루틴이 완료되었습니다 println(acquired) // 여전히 획득된 자원의 수를 출력합니다
Kotlin
복사
위 코드는 항상 0을 출력하며 자원이 누수되지 않는다.