Timeout
Coroutine의 실행을 취소하는 가장 명백하고 실용적인 이유는 실행 시간이 Timeout으로 설정한 시간을 넘어섰기 때문이다. 해당 Job에 대한 참조를 만들고 새로운 별도의 Coroutine을 실행해서 일정 시간 이후에 참조된 Job을 취소하는 과정을 거칠 수 있지만, 이러한 동작을 수행하는 withTimeout가 이미 만들어져 있다. 다음 예를 보자.
import kotlinx.coroutines.*
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에 의해 throw되는 TimeoutCancellationException은 CancellationException의 서브클래스이다. 우리는 이전에 이러한 스택 추적값이 인쇄된 것을 보지 못했다. 이는 CancellationException이 Coroutine이 완료되기 위한 일반적인 원인으로 간주되기 때문이다. 하지만, 위 예에서는 withTimeout을 main 함수 내부에서 사용했다.
취소는 단순한 Exception이기 때문에, 모든 리소스들은 일반적인 방식으로 닫힌다. 만약 시간 초과를 일으키는 동작들이나, withTimeout과 비슷하지만 시간 초과가 일어난다면 null이 return되는 withTimeoutOrNull 함수를 사용해야 한다면, try {...} catch (e: TimeoutCancellationException) {...} 블록으로 코드를 감싸는 방식을 사용할 수 있다.
import kotlinx.coroutines.*
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")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
코드를 실행할 때 더이상 Exception이 발생하지 않는 것을 확인할 수 있다.
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Cancellation and timeouts - Timeout
원문 최종 수정 : 2022년 6월 27일
비동기 Timeout과 리소스
withTimeout으로 발생되는 Timeout 이벤트는 현재 실행중인 블록의 코드와 비동기적으로 일어나며 언제든지 일어날 수 있다, 심지어 Timeout 블록에서 return이 일어나기 직전에서도 일어날 수 있다.*1 만약 블록 외부에서 닫거나 해제되어야 하는 일부 리소스를 블록 내부에서 열거나 획득해야 하는 경우 이 점을 염두에 두어야 한다.
닫을 수 있는 리소스를 Resouce 클래스를 사용하여 모방해보자. 이 클래스는 인스턴스화 될 때 acquired counter을 증가시키고 close 함수를 통해 이 counter을 감소시킴으로써, 얼마나 많은 수의 인스턴스가 생성되었는지를 추적한다. 작은 timeout을 가진 많은 수의 Coroutine을 실행시켜 withTimeout 블록 내에서 리소스를 얻고 약간의 delay 후에 바깥에서 닫도록 해보자.
import kotlinx.coroutines.*
var acquired = 0
class Resource {
init { acquired++ } // 리소스를 획득한다.
fun close() { acquired-- } // 리소스를 해제한다.
}
fun main() {
runBlocking {
repeat(100_000) { // 10만개의 Coroutine을 실행한다.
launch {
val resource = withTimeout(60) { // Timeout 기준시간을 60ms로 설정한다.
delay(50) // 50ms 동안 delay한다.
Resource() // 리소스를 획득하고 withTimeout 블록의 return 값으로 리소스를 반환한다.
}
resource.close() // 리소스를 해제한다.
}
}
}
// runBlocking 바깥은 모든 Coroutine 들이 완료된 다음 실행횐다.
println(acquired) // 획득되고 해제되지 않은 리소스들의 개수를 출력한다.
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
위 코드를 실행하면, 컴퓨터의 타이밍에 따라 다를 수 있지만 항상 0을 프린트 하지는 않는 것을 볼 수 있다. 0이 아닌 값을 확인하기 위해서는 이 예제에서 시간 초과 시간을 조정해야 할 수 있다.*2
📌 이 예제에서 10만개의 Coroutine에 대해 acquired counter의 증가와 감소는 Main Thread에서 실행되기 때문에 완전히 Safe하다. 이와 관련해서 추가적인 설명은 Coroutine Context에 대해 다루는 Chapter에서 설명할 것이다.
이러한 문제를 해결하기 위해서는 리소스를 withTimeout 블록에서 반환하는 대신 리소스에 대한 참조를 변수에 저장하는 방법을 사용할 수 있다.
fun main() {
runBlocking {
repeat(100_000) { // Launch 100K 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을 출력한다. 리소스가 누수되지 않는다.
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. withTimeout 블록 내부의 코드가 완료되기 직전을 뜻한다.
*2. 위 코드에서 숫자가 일치하지 않는 것은 withTimeout은 외부에 CancellationException을 전파해서 withTimeout 외부의 코드 또한 실행되지 않기 때문이다. 예를 들어 아래의 코드를 실행해보자.
fun main() {
runBlocking {
repeat(5) { // 5개의 Coroutine을 실행한다
launch {
val resource = withTimeout(60) { // Timeout 기준을 60ms로 설정한다.
println("before timeout")
delay(100) // 100ms 동안 delay한다.
}
println("after timeout")
}
}
}
}
코드가 실행되면 다음의 결과가 나온다.
after timeout이 출력되지 않는 것을 확인할 수 있다. withTimeout이 애러를 외부로 전파시켜 Coroutine이 종료되기 때문이다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Coroutines Timeouts - Asynchronous timeout and resources
원문 최종 수정 : 2022년 6월 27일
목차로 돌아가기