코틀린 코루틴 가이드 2 — Cancellation and Timeouts

김종식
7 min readFeb 6, 2021

(공식문서 바로가기)

Cancellation and Timeouts

이 장에서는 코루틴 취소와 타임아웃에 대하여 확인합니다.

Cancelling coroutie execution

장시간 동작되는 애플리케이션에서는 백그라운드에서 동작되는 코루틴에 대하여 세밀한 제어를 원할 것입니다. 예를들어 사용자는 코루틴을 시작한 페이지가 종료되었을 때, 그 결과는 더 이상 필요하지 않고 요청된 동작은 취소될 수 있습니다. launch 함수의 경우 코루틴에서 취소하는데 사용할 수 있는 Job을 반환합니다.

해당 코드를 실행 시키면 아래와 같은 결과를 확인할 수 있습니다.

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.

메인 코루틴 (위 코드에서는 runBlocking)이 job.cancel을 호출 즉시 해당 코루틴이 취소되었기 때문에 더 이상 출력 결과를 확인할 수 없습니다. 그리고 canceljoin 을 동작 처리하는 cancelAndJoin 확장 함수도 있습니다.

Cancellation is cooperative

코루틴은 취소 요청에 협조적(cooperative) 입니다. 코루틴 코드는 취소할 수 있어야 합니다. ‘kotlinx.coroutines’ 의 모든 중단함수들은 취소가 가능합니다.그들(*kotlinx.coroutines 의 중단함수)은 코루틴의 취소를 확인하고, 취소가 되었을 때 CancellationException 예외로 던지게 됩니다. 하지만, 만약 코루틴이 연산 작업중 취소 여부를 확인 하지 않으면, 예시처럼 취소 동작이 취소되지 않습니다.

예제 코드는 취소 후에도 5회 실행 되고 job이 종료됩니다.

Making computation code cancellable

연산중인 코드를 취소하는 것은 두 가지 방법이 가능합니다. 첫 번째는 취소를 확인하는 정지기능을 주기적으로 발동하는 것입니다. 이 방법을 달성하기 위한 적합한 함수로 yield() 가 있습니다. 다른 방법으로는, 명시적으로 취소 상태인지를 체크하는 것입니다. 후자의 방법으로 한번 접근 해 봅시다.

이전 예제에서 `while (i < 5)` 코드 대신 `while (isActive)` 로 변경하고, 다시 실행해 봅니다.

실행 결과 반복 동작이 취소된 것을 확인할 수 있습니다. isActive는 코루틴 내부에서 CoroutineScope 객체를 통해 접근이 가능한 확장프로퍼티 입니다.

Making computation code cancellable > coroutine yield()

코루틴을 정지시키려면 yield() 함수를 호출하면 됩니다. yield() 가 호출되면 , dispatcher 가 다음에 수행되어야 할 것을 결정합니다. 메인 코루틴과 2개의 서브루틴이 launch 되어 각각 yield() 를 실행해봅시다.

실행 결과는 다음과 같습니다.

main: start job 1
first job: work 1
second job: work 1
main: start job 2
first job: work 2
second job: work 2
main: start job 3
first job: work 3
second job: work 3

Closing resources with finally

취소가 가능한 중단함수의 경우, CancellationException 이 발생되므로 일반적인 방법을 통해 처리가 가능합니다. 예를들어, 코틀린의 `try {...} finally {...}` 표현으로 코루틴이 취소될 때 정상적으로 실행할 수 있습니다.

join()cancelAndJoin() 함수 모두 작업이 완료될 때 까지 기다리기 때문에,
위의 예시 실행 결과는 아래와 같이 출력됩니다.

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.

Run non-cancellable block

이미 CancellationException 이 발생한 코루틴의 finally 블록 안에서 중단함수를 호출할 경우, CancellationException 이 발생됩니다. 보통 파일을 종료하거나, 작업을 취소하거나, 통신 채널을 종료하는 것과 같은 종료되어야 하는 작업들은 넌블럭킹 (non-blocking) 으로 동작하지 때문에, 어떠한 중단함수로 동작되어도 문제가 되지 않습니다. 하지만, 이미 취소된 코루틴 안에서 이 후 동작이 동기적으로 중단함수를 호출해야 하는 상황이라면, 아래 예제처럼 `withContext(NonCancellable) {...}` 처럼 withContext 함수와 NonCancellable 컨텍스트를 사용를 사용하여 처리가 가능합니다.

Timeout

코루틴의 실행을 취소하는 실질적인 이유는 그 수행시간이 일정 시간을 초과한 경우입니다. (=일반적으로 어떤 작업을 취소시키는 이유는 그 실행 시간이 너무 길어져서 허용 시간을 넘어설 경우입니다.) Job에 대한 참조를 직접 다뤄서 별도의 코루틴을 실행 및 해당 루틴 동작 중 취소가 가능하지만, withTimeout 함수를 이용하여 처리할 수 있습니다. 예제를 실행 시켜보면 아래와 같은 결과를 확인할 수 있습니다.

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
CancellationException의 서브클래스 입니다. 우리는 이제까지 콘솔에서 스택트레이스가 출력된 것을 본 적이 없습니다. 왜냐하면, 메인 코루틴에서 취소된 경우 CancellationException 이 발생되어 정상적으로 메인 코루틴이 끝난 것으로 간주했기 때문입니다. 하지만, 이 예제의 경우 withTimeout을 메인 함수에서 사용했습니다.

취소는 단지 예외이기 때문에, 모든 자원은 통상적인 방법으로 닫히게 됩니다. `try {...} catch (e: TimeoutCancellationException) {...}` 을 활용하여 타임아웃 발생 시 예외처리를 하거나, withTimeout와 유사하지만 null을 리턴하는 withTimeoutOrNull 함수를 활용하여 타임아웃 발생에 대한 예외 처리가 가능합니다.

이 예제코드는 더 이상 코드 동작 중 exception 이 발생되지 않습니다.

Asynchronous timeout and resources

withTimeout 의 타임아웃 이벤트는 실행되는 코드 블록에 대하여 비동기적이며, 타임아웃 코드블록 수행 중 언제든지 발생될 수 있습니다. 코드 블록 내에서 자원을 열거나 획득할 경우 이 점을 유의해야 합니다.

예를들어, 자원을 획득하면 카운터를 증가시키고 종료 함수를 호출하면 카운터를 감소시키켜 얼마나 생성되었는지 추적이 가능한, 즉 자원 획득 및 종료를 모방하는 Resource 클래스가 있습니다. withTimeout 코드블록 내부에서 약간의 지연(delay) 후 이 리소스를 획득하며, 코드블록 밖에서 해제하는 코루틴을 약간의 timeout을 설정하여 대량으로 실행시켜 봅시다.

예제 코드에서 timeout 의 타이밍을 조정하여 항상 0이 출력되지 않는 것을 확인할 수 있습니다.

참고로 이 예제는 100_000 회 코루틴 실행에서 acquired 카운터를 증가 및 감소시키는 것은 동일한 메인 스레드에서 발생시키므로 완전히 안전합니다. 이와 관련하여 좀 더 자세한 내용은 다음 챕터인 Coroutine Context 에서 설명합니다.

이 문제를 해결하려면 withTimeout 코드 블록에서 리소스를 반환하는 대신
변수에 대한 참조를 리턴하도록 해야 합니다.

이 예제는 항상 0을 출력하고, 리소스는 릭이 발생되지 않습니다.

--

--

김종식

앱 개발자 / 꿈은 축구선수 / 쌍둥이 아빠