-
공식문서로 알아보는 Cancellation and timeoutsKotlin/코루틴 2023. 12. 4. 00:01
개요
Kotlin 공식문서를 보며 코루틴의 취소와 타임아웃에 대해 알아보고 실습도 진행해보고자 합니다.
코루틴의 취소
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.
오랫동안 실행되는 애플리케이션에서는 백그라운드 코루틴을 세밀하게 제어할 수 있어야 합니다.
예를 들어 사용자가 코루틴을 실행한 페이지를 닫았는데 이제 그 결과가 더 이상 필요하지 않으므로 작업을 취소할 수 있습니다.
그렇지 않으면 해당 작업에 대한 리소스가 낭비될 것입니다. (메모리 누수 발생 등)
launch 함수는 실행 중인 코루틴을 취소하는 데 사용할 수 있는 Job을 반환합니다.
main에서 job.cancel이 호출되면 해당 job의 코루틴이 취소되었기 때문에 더 이상 호출되기 않습니다.
job.cancleAndJoin()
취소와 동시에 join을 호출하는 cancleAndJoin 확장함수도 존재합니다.
suspend와 함께한다면 취소할 수 있다.
코루틴에서 취소는 cooperative(협력적)입니다.
이 말은 코루틴이 취소되기 위해서는 코루틴이 협력해야 한다는 의미있고, 모든 suspend 함수는 취소에 협력적입니다.
코루틴이 취소되었는지 확인하고 취소되었다면 CancellationException 예외를 던집니다.
하지만 내부 job이 취소할 수 없다면 내부적으로 계속 리소스를 소모합니다.
위의 예제에서는 delay()라는 suspend 함수를 사용했기 때문에 취소가 가능합니다.
suspend와 함께하지 않으면 취소할 수 없다.
suspend fun notCancelable(){ coroutineScope { 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.
내부적으로 suspend 함수가 사용되지 않았기 때문에 cancelAndJoin() 메서드가 호출되더라도 5번의 연산이 모두 수행된 후 종료됩니다.
만약 취소후에 예외를 잡아본다면
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}@1f266e5d job: I'm sleeping 3 ... kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@1f266e5d job: I'm sleeping 4 ... kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@1f266e5d main: Now I can quit.
JobCancellationException을 출력하는 모습을 볼 수 있습니다.
suspend가 없더라도 코루틴의 active 한 상황을 체크하여 취소가능하게 만들기
suspend fun cancelWithCheckingStatus(){ coroutineScope { val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 println("current status is = $isActive") while (isActive) { // cancellable computation loop // print a message twice a second if (System.currentTimeMillis() >= nextPrintTime) { println("job: I'm sleeping ${i++} ...") nextPrintTime += 500L } } println("current status is = $isActive") } 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.") } } //결과 current status is = true job: I'm sleeping 0 ... job: I'm sleeping 1 ... job: I'm sleeping 2 ... main: I'm tired of waiting! current status is = false main: Now I can quit.
취소 여부를 확인하는 함수를 주기적으로 호출하여 취소 상태를 명시적으로 확인할 수 있습니다.
isActive 필드로 확인할 수 있습니다.
isActive 필드는 코루틴 내부에서 CoroutinScope의 상태를 알려주는 확장 property입니다.
finally로 리소스 닫기 혹은 use 활용하기
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.
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) // 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 job: And I've just delayed for 1 sec because I'm non-cancellable main: Now I can quit.
이전 예제에서 코루틴을 시도하면 CancellationException이 발생합니다.
드물게 취소된 코루틴에서 일시중단을 수행해야 한다면 위의 예시처럼 withContext 함수와 NonCancellable 콘텍스트를 활용할 수 있습니다.
만약 withContext부분이 없다면 delay를 포함한 하위 부분은 수행되지 않습니다.
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.
타임아웃
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이 발생하게 됩니다.
타임아웃 발생 시 null이 반환
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
withTimeoutOrNull을 활용하게 되면 예외대신에 null이 반환됩니다.
timeout시 리소스가 반환되지 않을 수 있음
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 }
실제로 코드를 돌려보니 acquired가 33개 남아있습니다.
즉, 33개의 자원이 반환되지 않았습니다.
10K 코루틴에서 획득 카운터 증가 및 감소는 항상 동일한 스레드에서 발생하므로 완전하게 thread-safe 합니다.
리소스 누수가 없는 코드
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
finally를 활용하여 리소스를 닫아줍니다.
이제는 0이라는 결과가 잘 출력됩니다.
'Kotlin > 코루틴' 카테고리의 다른 글
코루틴 Timeout이 제대로 동작하지 않은 이유 (0) 2024.03.25 공식문서로 알아보는 Coroutine context and dispatchers (0) 2024.01.29 공식문서로 알아보는 Composing suspending functions (0) 2023.12.09 공식문서로 알아보는 Coroutines basics (0) 2023.12.03 Coroutines이란? (0) 2023.06.20