Cancelling coroutine execution
코루틴의 실행 취소
오랜시간동안 실행되는 애플리케이션에서 백그라운드 코루틴관리를 어떻게 해야할까?
사용자가 코루틴을 시작했던 페이지를 닫고, 더 이상 결과가 필요하지 않다면 해당 작업을 취소해야 할 것이다.
launch
함수를 사용하면 Job
을 리턴하고, 이것으로 실행 중인 코루틴을 취소할 수 있다.
코드
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancel() // cancels the job
job.join() // waits for job's completion
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이 호출되고 1.3초후 job.cancel
이 실행되어 job코루틴이 취소되었기에 job의 완료를 기다린 후 “main: Now I can quit.”가 출력되었다.
Cancellation is cooperative
코루틴의 취소는 협력적이다.
코루틴 코드가 취소가능하기 위해서는 협력적이어야 한다.
모든 중단 함수는 취소 가능하다.
코루틴의 취소를 확인하고 취소될 때 CancellationException
을 발생시킨다. 하지만, 코루틴이 작업중이고 취소를 확인할 수 없으면 취소될 수 없다.
코드
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
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 ...
main: Now I can quit.
위 코드는 취소했는데도 계속해서 I’m sleeping이 출력되고 있다.
즉, 코루틴이 계산 중인 경우 취소를 확인하지 않으면 계속 출력되는 것이다.
코드
val job = launch(Dispatchers.Default) {
repeat(5) { i ->
try {
// print a message twice a second
println("job: I'm sleeping $i ...")
delay(500)
} catch (e: Exception) {
// log the exception
println(e)
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
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!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@16d5e4dd
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@16d5e4dd
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@16d5e4dd
main: Now I can quit.
try - catch문을 추가한 결과, 취소 시 CancellationException
을 throw하고 있음을 볼 수 있다.
Making computation code cancellable
계산 코드를 취소 가능하게 만들기
- 취소를 확인하는 중단 함수를 주기적으로 호출하기
- yield 함수를 호출하기
- 취소 상태를 명시적으로 확인하기
코드
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
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.
이전 코드에서 while (i < 5)
를 대신해서 while (isActive)
를 사용한 코드이다.
isActive
는 CoroutineScope
객체를 통해 코루틴 내에서 사용할 수 있는 확장 프로퍼티이다.
Closing resources with finally
finally
를 사용해 종료하기
중단 함수는 CancellationException을 throw한다.
이때 try { … } finally { … }
와 코틀린의 use
함수는 코루틴이 취소될 때 작업을 실행한다.
1. try - 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) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
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.
2. use
Closable
을 상속받고 있는 객체에 use
를 사용하면 try/catch/finally가 포함되어 있어 자동으로 close된다.
Timeout
시간초과
실행시간을 초과했을 때 대부분 코루틴 실행을 취소할 것이다.
직접 Job
이나 launch
를 추적해 취소하는 방법 이외에도 withTimeout
을 사용할 수 있다.
코드
fun main() = runBlocking {
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
withTimeout
이 TimeoutCancellationException
을 throw한다.
TimeoutCancellationException
은 CancellationException
의 하위 클래스이다.
일반적인 예외처리와 동일하게 처리된다.
코드
fun main() = runBlocking {
val result = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
"Done" // will get cancelled before it produces this result
}
println("Result is $result")
}
실행 결과
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
이후 추가 동작을 실행하기 위해서는 예외를 throw하는 것 대신 withTimeoutOrNull
을 사용해 null을 반환할 수 있다.
Asynchronous timeout and resources
비동기 시간초과와 리소스
withTimeout
에서 타임아웃 이벤트는 블록 내에서 실행 중인 코드가와 비동기적으로 발생하거나 return직전에 일얼날 수 있다.
코드
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(10_000) { // Launch 10K coroutines
launch {
val resource = withTimeout(60) { // Timeout of 60 ms
delay(50) // Delay for 50 ms
Resource() // Acquire a resource and return it from withTimeout block
}
resource.close() // Release the resource
}
}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
}
실행 결과
0 또는 다른 값이 출력될 수 있다.
withTimeout{ … }
내부 코드가 모두 실행된 후에도 타임아웃이 발생해서 리소스 낭비가 일어날 수 있다.
이런 비동기 문제를 어떻게 해결할 수 있을까?
리소스를 withTimeout{ … }
에서 바로 return하지 않고, 변수를 따로 두면 리소스가 null이 아닌 경우에만 close가 동작한다.
코드
var acquired = 0
class Resource {
init { acquired++ } // Acquire the resource
fun close() { acquired-- } // Release the resource
}
fun main() {
runBlocking {
repeat(10_000) { // Launch 10K coroutines
launch {
var resource: Resource? = null // Not acquired yet
try {
withTimeout(60) { // Timeout of 60 ms
delay(50) // Delay for 50 ms
resource = Resource() // Store a resource to the variable if acquired
}
// We can do something else with the resource here
} finally {
resource?.close() // Release the resource if it was acquired
}
}
}
}
// Outside of runBlocking all coroutines have completed
println(acquired) // Print the number of resources still acquired
}
실행 결과
항상 0이 출력된다.
'Android > Coroutine' 카테고리의 다른 글
[Kotlin/coroutine] 5. Asynchronous Flow (1) (0) | 2023.10.25 |
---|---|
[Kotlin/Coroutine] 4. Coroutine context and dispatchers (0) | 2023.10.24 |
[Kotlin/Coroutine] 3. Composing suspending functions (2) | 2023.10.23 |
[Kotlin/Coroutine] 1. Coroutines basics (0) | 2023.10.19 |