[kotlin] job의 상태

문돌이 개발자·2024년 4월 10일
0

kotlin진심펀치

목록 보기
1/6
post-custom-banner

Job이란?

Job은 하나의 작업 단위를 의미한다.
즉 coroutine으로 처리하는 하나의 작업은 job 객체로 반환할 수 있다.

Job의 특징

Job에 대한 기본적인 설명으로는 Job은 부모-자식 관계의 계층 구조화 할 수 있으며 부모 Job이 cancel 되면 자식 Job들은 재귀적으로 모두 즉시 취소된다.
또한 CancellationException을 제외한 다른 예외로 자식 Job이 취소될 경우 부모 Job과 마찬가지로 자식 Job들이 모두 취소된다.

Job 생성

Job 개체를 생성하는 대표적인 두가지 방법은 코루틴 빌더를 통해 launch() 로 생성하는 방법과 Job()을 통해 생성하는 방법이 있다.
후자는 어떤 식으로 사용하는지 나중에 알아봐야겠다.

Job의 상태

coroutine을 사용하면서도 상태가 6개나 존재하는지 처음 알았다.

읽어보니 기본적으로 자식의 완료/취소 여부에 따라 상태가 변한다.
자기 작업이 완료되더라도 자식 작업들이 진행중이라면 Completing 상태가 된다.
자기 작업이 취소되더라도 자식 작업들이 취소되는 중이라면 Canceling 상태가 된다.
자신과 자식의 상태가 모두 완료되거나 취소되어야 Completed 혹은 Cancelled 상태가 된다.

Completing/Canceling의 경우 Job 내부에서 관찰가능한 상태이며 외부에서 볼 때 Completing/Canceling는 Active한 상태로 관찰된다.

이것저것 해보자

Job a를 만들고 상태를 확인해보자

fun main() {
    val a = CoroutineScope(Dispatchers.IO).launch {
        println("this is Job a: $this")
        launch {
            delay(3000)
        }
    }

    println("Job a isCompleted: ${a.isCompleted}")
    println("Job a isActive: ${a.isActive}")
    println("Job a isCancelled: ${a.isCancelled}")
}

결과

Job a isCompleted: false
Job a isActive: true
Job a isCancelled: false

바로 println을 찍기 때문에 isActive가 여전히 true이다.

이번엔 Job a가 완료될때까지 기다려보자

fun main() {
    val a = CoroutineScope(Dispatchers.IO).launch {
        println("this is Job a: $this")
        launch {
            delay(3000)
        }
    }

    runBlocking {
        a.join()
        println("Job a isCompleted: ${a.isCompleted}")
        println("Job a isActive: ${a.isActive}")
        println("Job a isCancelled: ${a.isCancelled}")
    }

}

결과

this is Job a: StandaloneCoroutine{Active}@bb49bf6
Job a isCompleted: true
Job a isActive: false
Job a isCancelled: false

Job a가 완료될때까지 기다렸다가 상태를 찍어보면 정상적으로 Completed상태를 확인할 수 있다.
cancel()도 한번 불러보자

fun main() {
    val a = CoroutineScope(Dispatchers.IO).launch {
        println("this is Job a: $this")
        launch {
            delay(3000)
        }
    }

    runBlocking {
        a.cancel()
        println("Job a isCompleted: ${a.isCompleted}")
        println("Job a isActive: ${a.isActive}")
        println("Job a isCancelled: ${a.isCancelled}")
    }
}
this is Job a: StandaloneCoroutine{Active}@15fcc313
Job a isCompleted: false
Job a isActive: false
Job a isCancelled: true

Cancelled를 확인할 수 있었다.

다시 문서를 확인해보니 cancel()을 호출한 경우도 정상적인 취소로 간주된다고 한다.

cancel()을 호출후 바로 상태를 찍어서 생기는 문제라고 판단해서 취소를 기다린 후 찍어보니

cancel()로 취소한 경우도 completed 상태가 되는 것을 확인할 수 있었다.

CancellationException을 던지면 어떻게 될까?

fun main() {
    val a = CoroutineScope(Dispatchers.IO).launch {
        println("this is Job a: $this")
        launch {
            delay(3000)
        }
        throw CancellationException()
    }

    runBlocking {
        a.join()
        println("Job a isCompleted: ${a.isCompleted}")
        println("Job a isActive: ${a.isActive}")
        println("Job a isCancelled: ${a.isCancelled}")
    }
}
this is Job a: StandaloneCoroutine{Active}@bb49bf6
Job a isCompleted: true
Job a isActive: false
Job a isCancelled: true

cancel()이 아니라 cancellationException을 던지게 되면 Completed와 Cancelled 상태가 동시에 true가 된다. 의도적으로 취소한 것과 cancellationException이 던져진 상태를 구분하기 위한 것 같다.
그러면 다른 예외를 한번 던져보자

fun main() {
    val a = CoroutineScope(Dispatchers.IO).launch {
        println("this is Job a: $this")
        try {
            launch {
                throw Exception()
            }
        }catch (e: Exception) {
            println("this is Job a: $this")
        }
        delay(3000)
    }

    runBlocking {
        a.join()
        println("Job a isCompleted: ${a.isCompleted}")
        println("Job a isActive: ${a.isActive}")
        println("Job a isCancelled: ${a.isCancelled}")
    }
}

결과

this is Job a: StandaloneCoroutine{Active}@bb49bf6
Exception in thread "DefaultDispatcher-worker-2" java.lang.Exception
	at job.JobKt$main$a$1$1.invokeSuspend(Job.kt:11)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@bb49bf6, Dispatchers.IO]
Job a isCompleted: true
Job a isActive: false
Job a isCancelled: true

Exception에 대한 정보가 나오는데 여기서 StandaloneCoroutine{Cancelling}@bb49bf6를 통해 Cancelling 상태를 확인할 수 있었다.

Job의 상태를 통해 작업을 핸들링할 경우에 유용하게 사용할 수 있을 것 같다.

정리

  • 부모는 자식들이 모두 완료되거나 취소되기 전엔 Cancelling or Completing
  • 부모가 취소되면 자식도 취소된다. 상태는 Cancelled
  • 자식이 cancellationException을 통해 취소 -> 상태는 Cancelled, Completed
  • 자식이 cancellationException 이외의 예외로 취소 -> 부모 작업 즉시 취소, 상태는 Cancelled

참고
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/

profile
까먹고 다시 보려고 남기는 기록
post-custom-banner

0개의 댓글