이 섹션은 코루틴 Cancellation과 Timeout에 대해 다룹니다.
Coroutine 실행 취소하기
긴 시간동안 실행되는 어플리케이션에서 백그라운드에서 실행되는 Coroutine에 대한 세밀한 제어가 필요할 수 있다. 예를 들어, 유저가 Coroutine을 실행시킨 페이지를 닫아 결과가 더 이상 필요하지 않아 작업이 취소되어도 되는 경우이다. launch 함수는 실행중인 코루틴을 취소하는 데 사용할 수 있는 Job 객체를 반환한다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // 약간의 시간 동안 delay 한다.
println("main: I'm tired of waiting!")
job.cancel() // Job을 cancel한다.
job.join() // Job의 실행이 완료될 때까지 기다린다.
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.
main 함수가 job.cancel을 호출하면, Job이 취소되었기 때문에 다른 코루틴의 출력을 확인할 수 없다. Job의 확장 함수로 cancel과 join 호출을 결합한 cancelAndJoin 또한 있다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Cancellation and timeouts - Cancelling coroutine execution
원문 최종 수정 : 2022년 6월 27일
Coroutines 취소는 협력적이다
Coroutine의 취소는 협력적이다. Coroutine 코드는 취소 가능하도록 협력해야 한다. kotlinx.coroutines 패키지의 모든 일시 중단 함수들은 취소 가능하다. 그들은 Coroutine이 취소되었는지 확인하고 취소되었을 경우 CancellationException을 발생시킨다. 만약 코루틴이 계산 작업 중이고 취소를 확인하지 않는다면, 다음의 예시처럼 취소될 수 없다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 계산 루프, CPU를 낭비한다
// 1초에 두 번 메세지를 출력한다.
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 약간의 시간 동안 delay 한다
println("main: I'm tired of waiting!")
job.cancelAndJoin() // Job을 취소하고 실행이 완료될 때까지 기다린다.
println("main: Now I can quit.")
}
전체 코드는 이곳에서 확인할 수 있습니다.
코드를 실행하여 취소가 실행된 이후에도 다섯번의 반복 후에 Job이 완료될 때까지 "I'm sleeping"이 계속해서 프린트 되는 것을 보자.
같은 문제가 CancellationException을 catch 하고 다시 throw 하지 않는 경우에도 생긴다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
repeat(5) { i ->
try {
// 1초에 두 번 메세지를 출력한다.
println("job: I'm sleeping $i ...")
delay(500)
} catch (e: Exception) {
// 예외를 로깅한다.
println(e)
}
}
}
delay(1300L) // 약간의 시간 동안 delay 한다.
println("main: I'm tired of waiting!")
job.cancelAndJoin() // Job을 취소하고 완료될 때까지 기다린다.
println("main: Now I can quit.")
}
전체 코드는 이곳에서 확인할 수 있습니다.
예외를 catch 하는 것은 안티 패턴이지만, 이 문제는 CancellationException을 다시 throw 하지 않는 runCatching 함수를 사용하는 경우와 같이 미묘한 경우에도 일어날 수 있다.
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*그림1은 공식 가이드에 없는 그림 입니다. 이해를 위해 추가하였습니다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Cancellation and timeouts - Cancellation is cooperative
원문 최종 수정 : 2022년 6월 27일
Coroutine의 Computation 코드를 취소 가능하게 만들기
computation code를 취소 가능하게 만드는 두가지 접근 방식이 있다. 첫 째는 주기적으로 일시 중단 함수를 실행시켜서 취소되었는지 확인하도록 하는 것이다. 이 목적을 위해 좋은 방식인 yield 함수가 있다. 다른 하나는 명시적으로 취소 상태를 확인하도록 하는 것이다. 후자의 접근 방식을 시도해보도록 하자.
이전 예시*1의 while (i < 5)를 while (isActive)로 변경한 후 다시 실행 시켜보도록 하자.
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // 취소 가능한 computation loop
// 1초에 두 번 메세지를 출력한다.
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 약간의 시간 동안 delay 한다
println("main: I'm tired of waiting!")
job.cancelAndJoin() // Job을 취소하고 실행이 완료될 때까지 기다린다.
println("main: Now I can quit.")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이제 이 loop가 취소되는 것을 볼 수 있다*2. isActive는 CoroutineScope 객체를 통해 Coroutine 내부에서 사용할 수 있는 확장 프로퍼티이다.
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 이전 예시 코드는 다음과 같다. 이 코드의 while 문 내부에서는 Cancellation을 확인할 수 있는 부분이 없다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 계산 루프, CPU를 낭비한다
// 1초에 두 번 메세지를 출력한다.
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 약간의 시간 동안 delay 한다
println("main: I'm tired of waiting!")
job.cancelAndJoin() // Job을 취소하고 실행이 완료될 때까지 기다린다.
println("main: Now I can quit.")
}
*2. 바뀐 코드를 실행한 결과는 다음과 같다.
* yield를 사용해도 같은 결과가 나온다. 별도 글로 분리 예정
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 계산 루프, CPU를 낭비한다
yield()
// 1초에 두 번 메세지를 출력한다.
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 약간의 시간 동안 delay 한다
println("main: I'm tired of waiting!")
job.cancelAndJoin() // Job을 취소하고 실행이 완료될 때까지 기다린다.
println("main: Now I can quit.")
}
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Cancellation and timeouts - Making computation code cancellable
원문 최종 수정 : 2022년 6월 27일