취소
•
오래 실행되는 어플리케이션에서는 백그라운드 코루틴에 대해 세밀한 제어가 필요할 수 있다.
•
예를 들어, 사용자가 코루틴을 시작한 페이지를 닫았을 때 그 결과는 더 이상 필요하지 않으며 작업을 취소할 수 있다.
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을 출력하며 자원이 누수되지 않는다.