일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- architecture
- Coroutines
- 암호학
- Hilt
- TEST
- pandas
- Kotlin
- boj
- android
- ProGuard
- textfield
- 코루틴
- Gradle
- MiTweet
- 쿠링
- 코드포스
- relay
- MyVoca
- activity
- AWS
- Coroutine
- Codeforces
- Rxjava
- Compose
- 백준
- Python
- 프로그래머스
- androidStudio
- GitHub
- livedata
- Today
- Total
이동식 저장소
[Kotlin] Coroutines - Cancellation and Timeouts 본문
코루틴 취소하기
코루틴을 취소해야 할 경우가 있다. 예를 들어 어떤 페이지가 열려 있을 때 작동하는 코루틴이 있는데, 사용자가 그 창을 닫았다고 해 보자. 창이 닫혔으므로 코루틴은 더 이상 작동할 필요가 없으며 (따라서) 중지하는 것이 바람직하다.
launch
메소드가 반환하는 Job
객체를 이용하여 코루틴을 제어할 수 있다.
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 취소
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.
Job.cancel
메소드를 이용하여 해당 코루틴을 멈출 수 있다. 다만 코루틴이 종료하기까지 시간이 필요할 수 있기 때문에, Job.join
메소드를 이용하여 코루틴이 멈출 때까지 기다려야 한다. 아니면 간결하게 Job.cancelAndJoin
메소드를 사용할 수도 있다.
무작정 취소되는 것은 아니다
물론 모든 코루틴을 이렇게 취소할 수 있는 것은 아니며, 코루틴이 협조할 때만 취소할 수 있다. 예를 들어 kotlinx.coroutines
의 모든 suspending 함수는 취소 가능하다. 현재 코루틴에 취소 명령이 내려졌는지 지속적으로 확인하기 때문이다. 그러나 코루틴이 계산하기 바빠서 자신에게 취소 명령이 내려졌는지 확인하지 않는다면, 코루틴은 취소되지 않는다.
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
// 1초에 두 번씩 출력
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancel()과 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!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
위의 코드를 보면 job
은 어떠한 suspending 함수도 사용하지 않기 때문에 자신에게 취소 명령이 내려졌는지 확인할 수 없고, 따라서 job.cancelAndJoin()
후에도 계속 작동하게 된다.
취소할 수 있게 하려면
첫 번째 방법으로는 suspending 함수를 주기적으로 호출하는 방법이 있다. 이러한 목적에 맞는 yield()
메소드가 있으니 필요하면 사용하도록 하자.
두 번째 방법으로는 코루틴 내부에서 자신이 취소 대상인지 확인하는 것이다. 여기서는 두 번째 방법을 사용하도록 하겠다.
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) { // this: CoroutineScope
var nextPrintTime = startTime
var i = 0
while (isActive) { // 취소 대상인지 확인
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.")
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.
CoroutineScope
객체의 isActive
프로퍼티를 사용하여 코루틴이 계속 작동할 수 있는지 확인하였다.
finally
사용하기
취소될 수 있는 suspending 함수는 CancellationException
을 던진다. 코루틴에서 파일 등의 리소스를 사용하는 경우 코루틴이 취소될 때 리소스를 해제할 필요가 있는데, try {...} finally {...}
또는 코틀린의 use
함수를 사용하면 좋다.
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L) // suspending function
}
} 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.
join
과 cancelAndJoin
은 모두 finally
구문이 실행될 때까지 기다려 준다. 작업을 종료할 때 꼭 실행해야 하는 코드가 있다면 위처럼 해 보자.
finally
로 안 되는 경우
finally
블럭에서 사용하는 코드는 대부분 파일을 닫고 job
을 취소하는 등의 non-blocking 코드이다. 그러나 finally
안에서 suspending 함수를 호출해야 하는 경우가 있을 수 있다. 그런데 코루틴이 취소 대상인 경우에는 suspending 함수를 실행하면 CancellationException
을 던진다고 했다. Exception
을 처리하는 과정에서 똑같은 Exception
이 발생하는 것이다.
이런 경우에는 suspending 함수를 호출하는 부분을 withContext(NonCancellable) {...}
을 사용하여 취소 불가능한 코드로 만들어야 한다.
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.")
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.
시간 초과
보통 코루틴을 취소하는 이유는 작업이 너무 오래 걸려서인 경우가 많다. 예를 들어 웹 페이지가 5초 안에 로드되지 않는다면 로드를 멈추고 사용자에게 에러 메시지를 띄워줘야 할 때가 있다. 이런 경우에는 withTimeout
메소드를 사용하여 간편하게 timeout을 처리할 수 있다.
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
은 인자로 주어진 시간(ms) 안에 코루틴이 종료되지 않으면 TimeoutCancellationException
을 던진다. 이 예외는 CancellationException
을 상속받았기 때문에 위에서와 동일한 방법으로 핸들링할 수 있다. 당연히 try catch
로 핸들링해도 된다.
fun main() = runBlocking {
try {
withTimeout(1300L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
} catch (e: CancellationException) { // 또는 TimeoutCancellationException
println("Cancel!")
}
}
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Cancel!
또는 withTimeoutOrNull
메소드를 사용할 수도 있다. 이 메소드는 withTimeout
과 유사하지만 TimeoutCancellationException
이 발생하면 null
을 반환한다.
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
timeout은 비동기적이다
withTimeout
에서 timeout은 실행되는 코드와 비동기적으로 발생한다. 코드가 종료되기 직전에 timeout이 발생할 수도 있다는 뜻이다. 코루틴 내부에서 외부의 리소스를 사용하는 경우 이 점에 주의할 필요가 있다.
다음의 코드를 보자. 리소스를 흉내내는 Resource
클래스를 정의하였다. 이 클래스는 단순히 현재 몇 개의 객체가 만들어졌는지 세는 역할을 한다. 이제 Resource
객체를 만들고 닫는 코루틴을 10만 번 실행해 보자. 이때 withTimeout(60)
과 delay(50)
를 사용하여 단 10ms 안에 객체를 만들어 반환하도록 하였다.
var acquired = 0
class Resource {
init { acquired++ } // 리소스 생성
fun close() { acquired-- } // 리소스 해제
}
fun main() {
runBlocking {
repeat(100_000) { // 10만 번 실행
launch {
val resource = withTimeout(60) { // timeout
delay(50)
Resource() // 리소스 생성하여 반환
}
resource.close() // 반환된 리소스 해제
}
}
}
println(acquired) // 해제되지 않은 리소스의 개수 출력
}
17
CPU의 상황에 따라 객체를 만들었지만 반환하려는 찰나에 timeout이 발생할 수도 있다. 코드를 실행해 보면 매번 결과가 달라지는 것을 확인할 수 있다.
이 문제를 해결하려면 Resource
객체가 반환되지 않을 수도 있음을 인정해야 한다. 따라서 반환이 아니라 할당 형식을 사용해야 한다.
runBlocking {
repeat(100_000) {
launch {
var resource: Resource? = null // 참조 변수
try {
withTimeout(60) {
delay(50)
resource = Resource() // 리소스를 만들어서 참조함
}
// 리소스 작업...
} finally {
resource?.close() // 리소스가 만들어진 경우에만 해제
}
}
}
}
println(acquired) // 해제되지 않은 리소스의 개수 출력
이렇게 하면 항상 0이 출력된다. 메모리 누수를 해결하였다.
0
참고 문헌
'Primary > Kotlin' 카테고리의 다른 글
[Kotlin] Coroutines - Context and Dispatchers (0) | 2021.01.21 |
---|---|
[Kotlin] Coroutines - Suspending 함수 활용하기 (0) | 2021.01.20 |
[Kotlin] Coroutines - Basics (0) | 2021.01.19 |
[Kotlin] Sequence (0) | 2021.01.15 |
[Kotlin] Thread 생성 및 실행 (0) | 2021.01.14 |