Kotlin

Kotlin Coroutine Job

dev_roach 2022. 7. 1. 20:29
728x90

Jobs

코루틴에서 Job 이란 무엇일까? 컨셉적으로는 LifeCycle 에서 취소될 수 있는 것을 뜻한다. Job 을 알아야 하는 이유는 내가 알기론, 모든 ㄴKotlin Coroutines Library 를 이용해 만든 CoroutineBuilders 는 Job 을 만든다. Deffered 또한 Job Interface 를 상속하고 있다.

Structured Concurrency

Job 또한 코틀린의 기본원칙인 Structured Concurrency 를 따른다. 만약, Structured Concurrency 를 모른다면, 다시 이전포스트를 공부하고 오는게 좋다. 하여튼, 그래서 아래 Job 이 취소되거나, 부모 Job 이 취소될 경우 그 Scope 는 Cancel 될 것이다.

Job LifeCycle

image

Job 의 LifeCycle 은 위와 같다. 위의 사진은 Kotlin Coroutines 책 에서 가져온 사진이다. 대부분의 Coroutine 들은 "Active" 상태로 시작하지만, Job 은 특이하게도 "NEW" 라는 상태로 시작한다. 그래서 Lazily 하게 실행될 수 있다. Job 이 실행되게 되면 상태는 Active 로 바뀌게 된다. 여기서 Job 이 성공적으로 성공했다면 Completing 상태로 돌아가고, Children 이 끝나는 걸 대기한다. 모든 Children 들이 종료되면, Job 은 Completed 상태가 된다. 만약 Active 상태 이후로 실패되거나 취소 되면, Cancelling 상태로 변경된다.

보통 Network I/O 로직에 Coroutine 을 사용하는데 이때 Cacelling 상태가 되었을때, Session 을 끊는 다던가, Resource 를 반환한다거나 하는 일들을 수행하면 좋다고 한다. 이것마져 끝나면 "Cancelled" 상태가 된다.

Lazily Start

아까 위에서 설명했듯이 New 라는 State 가 존재하기에 Job 은 Lazily 하게 실행할 수 있다.

// launch started lazily is in New state
val lazyJob = launch(start = CoroutineStart.LAZY) { delay(1000) }
println(lazyJob) // LazyStandaloneCoroutine{New}@ADD
// we need to start it, to make it active
lazyJob.start()
println(lazyJob) // LazyStandaloneCoroutine{Active}@ADD
lazyJob.join() // (1 sec)
println(lazyJob) //LazyStandaloneCoroutine{Completed}@ADD

The properties(isActive, isCompleted, isCancelled) have difference value each State

아래 표를 보면 알 수 있듯이, 각 상태마다 속성이 나타내는 값이 다르다. 코루틴을 사용하려면 잘 알고 있는게 좋은 값들이다.

image

Parent 의 잡을 상속할까?

우리가 앞에서 배워왔던 것을 생각하면 하나의 CoroutineContext 안의 자식 객체들은 모두 같은 Job 을 가질 것 같기도 하다. 왜냐하면, 모든 CoroutineBuilders 들은 Job 을 만드니까, 하지만 아니다. 그 이유는 CoroutineContext 는 자신만의 Job을 소유하기 때문이다.

fun main(): Unit = runBlocking {
    val name = CoroutineName("Some name") val job = Job()
    launch(name + job) {
        val childName = coroutineContext[CoroutineName] println(childName == name) // true
        val childJob = coroutineContext[Job] println(childJob == job) // false println(childJob == job.children.first()) // true
    } 
}

위의 코드를 보면 한가지 사실을 알 수 있는데, Parent 는 Childrens 의 Job 을 모두 소유하고 있다. 그래서 아래와 같은 코드로도 사용이 가능하다.

fun main(): Unit = runBlocking {
    launch {
        delay(1000)
        println("Test1")
    }
    launch {
        delay(2000)
        println("Test2")
    }
    println("All tests are done")

    val children = this.coroutineContext[Job]?.children
    val job = this.coroutineContext[Job]
    val NumbersOfChildren = children?.count()

    println("The Job($job) have $NumbersOfChildren children")
}

Parent 의 Job 을 왜 상속받지 못할까?

근데 왜 Parent Job 을 상속받지 못할까? 그 이유는, Structured Concurrency 가 허용될 수 없기 때문이다. 아래 코드를 한번보자.

fun main(): Unit = runBlocking {
    launch(Job()) { // the new job replaces one from parent
        delay(1000)
        println("Will not be printed")
   }
}

이 함수는 어떻게 실행될까? println 이 호출될까? 아니 호출되지 않는다. Job() 을 통해 새롭게 생성한 CoroutineContext 는 Parent 와 아무런 연관이 없기 때문에, Parent 는 자식의 실행이 끝마치기를 기다리지 않는다.

Job Factory Function

우리는 계속해서 Coroutine Builder 로만 Job 을 생성하는 걸 봐왔었는데, Job 은 사실 다른 방식으로도 생성이 가능하다. 우리가 위에서 사용했던 Job() 이라는 Factory Function 을 이용해서도 가능하다. 하지만 이렇게 생성한 Job 은 위에서 예제로 설명했듯이, 어떠한 CoroutineContext 에 속해있던 그것들과 관계가 맺어져있지 않다. 즉, Structured Concurrency 가 되지 않을 수 있다는 것이다. 그래서 Job 을 Factory Function 으로 만들게 되면 실수하거나, 코드를 제대로 예측하지 못할 수 있다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) { 
        delay(1000)
        println("Text 1")
    }
    launch(job) { 
        delay(2000)
        println("Text 2")
    }
    job.join() 
    println("Will not be printed")
}

이 코드의 실행은 어떻게 될까? 이 코드는 끝나지 않는다. 왜냐면, job() 은 일단 Cancellable 이나 Completed 상태가 되지 않는다면 계속해서 Active 상태이다. 그래서 현재 상태에서는 Job 에 주어진 body 가 없으므로 계속해서 Active 상태로 존재한다. 이 Code 를 끝내기 위해서는 어떻게 해야 할까?

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Job 1 = $job")
        println("Text 1")
    }
    launch(job) {
        delay(2000)
        println("Job 2 = $job")
        println("Text 2")
    }
    job.children.forEach {
        it.join()
    }
    println("Will be printed")
}

이렇게 작성하면 끝난다. 근데 하나 짚고 넘어가야 할게 있다. Structured Concurrency 를 완전히 안준수하는 것일까? 과연 아래코드는 어떻게 될까?

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        throw IllegalArgumentException("")
    }
    launch(job) {
        delay(2000)
        println("Job 2 = $job")
        println("Text 2")
    }
    job.join()
    println("Will not be printed")
}

아래 코드는 코드가 종료된다. 그 이유는 Job 의 children 에서 Exception 이 전파됬기 때문이다. 따라서 위의 코드를 보면 완전히 Structured Concurrency 를 준수하지 않는 것은 아니다.

Job.completed()

이 메소드를 사용하게 되면, job 의 children 들은 completed 가 될때까지 동작하며, 호출한 이후로는 다른 코루틴은 참가할 수 없게 된다. completed 의 result 가 true 면 이 job 이 끝난것이고, false 면 이미 끝난 것이다. 일단 예제를 보면 훨씬 더 이해가 잘가니 아래 예제를 보자.

일단 정상적인 케이스는 아래와 같다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }
    launch(job) {
        delay(2000)
        println("Job 2 = $job")
        println("Text 2")
    }
    println("Is Complete? ${job.complete()}")
    job.join()
    println("Will be printed")
}

// true
// Text 1
// Text 2
// Will be printed

그럼 completed 호출 후에 다른 Children 하나를 attach 해보자.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }
    launch(job) {
        delay(2000)
        println("Job 2 = $job")
        println("Text 2")
    }
    println("Is Complete? ${job.complete()}")

    job.join()

    launch(job) {
        println("Text 3")
    }

    println(job.children.count()) // 0

    job.join() // not executed

    println("Will not be printed")
}

//Is Complete? true
//Text 1
//Job 2 = JobImpl{Completing}@3a71b63e
//Text 2
//0
//Will not be printed

위의 실행결과를 봐서 알겠지만 전혀 실행되지 않는다.

 

깃허브에 저장된 예제 코드들

https://github.com/tmdgusya/kotlin-coroutines-example

 

GitHub - tmdgusya/kotlin-coroutines-example

Contribute to tmdgusya/kotlin-coroutines-example development by creating an account on GitHub.

github.com

 

728x90

'Kotlin' 카테고리의 다른 글

CoroutineScope 과 Runblocking 의 차이  (0) 2022.07.05
Kotlin Coroutine Exception Handling  (0) 2022.07.05
Kotlin) 변성  (0) 2022.06.30
Coroutine Builder  (0) 2022.06.27
Kotlin Coroutines  (1) 2022.06.19