본 글은 조세영님의 <코틀린 코루틴의 정석>을 읽고 이를 바탕으로 작성되었습니다.
앞선 글에서 코루틴을 사용하는 이유와 생성, 코루틴 디스패처에 대해 알아보았다면, 본격적으로 코루틴의 처리, 취소에 대해 알아보자.
코루틴 빌더 함수
- launch
- runBlocking
→ 모든 코루틴 빌더 함수는 코루틴을 생성하고 코루틴을 추상화한 Job객체를 생성한다
코루틴의 순차 처리
코루틴을 사용하다 보면 그 순서를 정해서 순차적으로 처리해야하는 일이 빈번하게 발생한다. 예를 들어 갱신된 토큰값을 받아온다던가, 네트워크 연결 후 결과값을 받아온다던가, 데이터 베이스에서 작업을 순차적으로 처리해야한다던가 등이다.
그러면 코루틴은 그 순서를 어떤식으로 보장해서 처리할까 ?
join을 사용하는
- 함수를 호출한 코루틴은 join의 대상이 된 코루틴(job)이 완료될때까지 일시 중단된다
- 따라서 join은 코루틴 등 일시 중단이 가능한 지점에서만 호출될 수 있다
- 이때 join은 join을 호출한 코루틴만 중단시키는거지 이미 실행중인 다른 코루틴을 중단시키지는 않는다
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100L)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
updateTokenJob.join() // updateTokenJob이 완료될 때까지 일시 중단
val networkCallJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#3] 네트워크 요청
*/
joinAll을 사용하는
- 복수의 코루틴의 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단시킨다
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
이렇게 코루틴을 순차처리하는 방법을 알아봤다면 원하는 시점에 코루틴을 실행시키는 방법, 즉 코루틴을 지연 시작하는 법도 알아보자.
코루틴의 지연시작
CoroutineStart.LAZY를 이용해 코루틴 지연하기
- 코루틴을 생성해놓고 나중에 지연 시작하게 하고 싶은 경우 이를 사용한다.
- launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨서 옵션을 적용한다
- 생성 후 대기 상태에 놓이고, 실행을 요청해야 시작된다
- LAZY로 launch해 생성하고, .start()를 통해 실행시켜야 시작된다
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 지연 실행")
}
delay(1000L)
lazyJob.start() // lazyJob 코루틴 실행하게 ! (결국 1초 뒤 시작 -> 직접 실행 요청 필요)
}
/*
// 결과:
[main @coroutine#2][지난 시간: 1014ms] 지연 실행
*/
코루틴 취소하기
코루틴을 생성하고, 처리하고 시점을 조절한다면 당연히 취소하는 방법도 있을 것이다.
cancel 사용해 코루틴 취소
- 원하는 코루틴에 .cancel()을 통해 특정 시점에 취소할 수 있다.
certainJob.cancel()
그러면 특정 코루틴을 취소하고 그 이후 바로 어떤 작업을 실행시켜야 하는 상황이 있다면, cancel() 후 launch()하면 제대로 동작할까 ?
- 아니다. cancel()하면 코루틴은 즉시 취소되는 것이 아니라 job객체에 취소 확인용 플래그를 ‘취소 요청됨’으로 변경함으로서 취소해야함을 알린다.
- 따라서 cancel()이 호출된다고 즉시 코루틴이 취소될 것임을 보장할 수 없다.
- 그러므로 cancel() 후 launch() 의 실행순서가 코드작성 순서대로임을 보장할 수 없다.
이렇게 취소 후 실행의 순서를 보장해 순차처리하기 위해 cancelAndJoin이 사용된다.
cancelAndJoin 사용해 순차처리
- cancelAndJoin의 대상이된 코루틴이 취소될 때 까지 호출부의 코루틴이 중단된다.
- 따라서 아래 코드를 보면 executeAfterJobCancelled()는 longJob 코루틴이 취소 완료된 후에 실행될 수 있다.
fun main() = runBlocking<Unit> {
val longJob: Job = launch(Dispatchers.Default) {
}
longJob.cancelAndJoin() // longJob이 취소될 때까지 runBlocking 코루틴 일시 중단
executeAfterJobCancelled()
}
fun executeAfterJobCancelledfun executeAfterJobCancelled() {
() {
// 이 함수는 longJob이 취소된 후 실행될 것
}
앞서 잠깐 언급했는데 cancel()이 호출되면 취소 확인용 플래그를 ‘취소 요청됨’으로 변경한다고 했다.
결국 코루틴이 취소 완료가 되려면 코루틴이 이 플래그를 확인해야 가능하다는 것이다.
그러면 코루틴이 이 취소 플래그를 확인하는 시점은 언제일까 ?
바로 일시 중단 시점이나 실행을 대기 하는 시점이다. 그리고 코루틴이 취소되도록 만드는, 즉 취소를 확인하는 방법에는 세가지가 있다.
코루틴의 취소 확인 - delay, yield, isActive
delay를 이용한 취소 확인
취소 확인은 일시 중단 시점, 실행 대기 시점 이 두가지에서 할 수 있다.
그렇다면 delay에서는 어떻게 가능할까? 답은 delay의 내부코드에 있다
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
- delay는 suspend함수, 즉 일시 중단 함수로 구현되어있다.
- 따라서 delay되는 동안 일시 중단되므로 이때 코루틴의 취소를 확인할 수 있다
- 하지만 이 방법은 while문이 반복될 때마다 작업을 강제로 중단시키므로 효율적이지 않고, 프로그램의 성능 또한 저하시킨다
yield를 이용한 취소 확인
- yield()는 자신이 사용하던 스레드를 양보한다.
- 스레드 사용 양보는 곧 스레드 사용의 중단이므로 일시 중단 상태에 들어간다. 따라서 이때 취소를 확인할 수 있다
- 하지만 아무리 코루틴이 경량 스레드라고 하더라도 매번 일시 중단되는 것은 비효율적인 작업일 수 있다
CoroutineScope.isActive를 사용한 취소 확인
- coroutineScope는 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 프로퍼티인 isActive를 제공한다.
- 따라서 while문의 인자로 IsActive를 넣어 취소 확인 코드를 넣어주면 코루틴이 잠시 멈추거나 양보하지 않으면서도 취소 확인을 할 수 있어서 효율적이다 !
결론적으로, 코루틴의 취소는 일시 중단 시점 / 실행 대기 시점이 생겨야 가능하다는 것을 기억하고, 이 시점이 없다면 위의 세가지 방법을 통해 취소를 확인해주어 코루틴을 취소할 수 있게 만들어주어야 한다.
코루틴의 상태와 job상태변수
코루틴의 상태
코루틴은 다음과 같이 6가지 상태를 가질 수 있다.
생성
- launch, runBlocking으로 생성 → 생성하면 바로 실행중으로 넘어간다
- 바로 실행안하게 하려면 앞서 언급한 LAZY로 지연 시작시키면된다
실행중
- 실제 실행 중 + 일시 중단 상태 모두
취소중
- cancel()을 통해 취소를 요청했을 경우
취소완료
- 취소 확인 시점에 취소가 확인된 경우
실행완료
- 코루틴의 모든 코드가 실행 완료된 경우
- 이러한 상태들을 나타내기 위해 코루틴은 외부로 공개하는 3가지 boolean 상태 변수를 가진다
코루틴의 상태 변수
isActive
- 코루틴의 활성화 여부 → 실행 중, 취소 요청
isCancelled
- 코루틴의 취소 요청됐는지 여부 → 취소 요청, 취소 완료
isCompleted
- 코루틴의 실행 완료 여부 → 실행 완료, 취소 완료
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L) // 1초간 대기
}
delay(2000L) // 2초간 대기
printJobState(job)
}
- 1초 동안 실행되는 코루틴인데 이를 2초 동안 대기했으니까 이미 실행 완료 → 따라서 isCompleted만 true
이를 코루틴의 상태별로 job의 상태를 체크해 정리하면 다음과 같다.
상태 | isActive | isCancelled | isCompleted |
생성 | false | false | false |
실행중 | true | false | false |
실행완료 | false | false | true |
취소중 | false | true | false |
취소완료 | false | true | true |
이번 포스트서는 코루틴의 처리와 시작, 취소에 대해 알아보았다. 또, 코루틴과 job의 상태에 대해 잘 알아두고 코루틴 내부에서 어떤 상태 전이가 일어나는지 제대로 알면 코루틴의 제대로 된 활용에 도움이 될 것 같다.
'코틀린 > Kotlin Coroutine' 카테고리의 다른 글
[Kotlin][Coroutine] Flow의 이해 (0) | 2024.07.06 |
---|---|
[Kotlin][Coroutine] CoroutineContext (0) | 2024.07.02 |
[Kotlin][Coroutine] Async, Await, Deferred, withContext (0) | 2024.07.02 |
[Kotlin][Coroutine] CoroutineDispatcher와 ThreadPool (0) | 2024.05.18 |
[Kotlin][Coroutine] 멀티스레드와 코루틴 (0) | 2024.05.18 |