반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
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
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코틀린 코루틴] 8장. 잡과 자식 코루틴 기다리기 본문

Kotlin/코틀린 코루틴

[코틀린 코루틴] 8장. 잡과 자식 코루틴 기다리기

어리둥절범고래 2024. 3. 30. 14:54
반응형

코루틴 부모-자식 관계의 특성은 다음과 같습니다.

 

  • 자식은 부모로부터 컨텍스트를 상속받는다.
  • 부모는 모든 자식이 작업을 마칠 때까지 기다린다.
  • 부모 코루틴이 취소되면 자식 코루틴도 취소된다.
  • 자식 코루틴에서 에러가 발생하면, 부모 코루틴 또한 에러로 소멸한다.

 

 

Job이란 무엇인가?

 

잡(job)은 수명을 가지고 있으며, 취소 가능합니다.

 

잡(코루틴) 상태 도식도

Active 상태에서는 잡이 실행되고 코루틴은 잡을 수행합니다.

지연 시작되는 코루틴을 제외한 대부분의 코루틴은 Active 상태로 시작하며, 지연 시작되는 코루틴만 New 상태에서 시작됩니다.

New 상태인 코루틴이 Active 상태가 되려면 작업이 실행되어야 하며, 코루틴이 본체를 실행하면 Active 상태로 가게 됩니다.

실행이 완료되면 상태는 Completing으로 바뀌고 자식들을 기다립니다.

자식들도 모두 실행이 완료되면 잡은 마지막 상태인 Completed로 바뀝니다.

만약 잡이 실행 도중(Active 또는 Completing 상태) 취소되거나 실패하게 되면 Cancelling 상태가 됩니다.

일반적으로 Cancelling 상태에서 연결을 끊거나 자원을 반납하는 등의 후처리 작업을 합니다.

후처리 작업이 완료되면 잡은 Cancelled 상태가 됩니다.

 

suspend fun main() = coroutineScope {
    val job = Job()
    println(job) // JobImpl{Active}@6433a2
    job.complete()
    println(job) // JobImpl{Completed}@6433a2

    val activeJob = launch {
        delay(1000)
    }
    println(activeJob) // StandaloneCoroutine{Active}@3f8f9dd6
    activeJob.join()
    println(activeJob) // StandaloneCoroutine{Completed}@3f8f9dd6

    val lazyJob = launch(start = CoroutineStart.LAZY) {
        delay(1000)
    }
    println(lazyJob) // LazyStandaloneCoroutine{New}@5f2b9338
    lazyJob.start()
    println(lazyJob) // LazyStandaloneCoroutine{Active}@5f2b9338
    lazyJob.join()
    println(lazyJob) // LazyStandaloneCoroutine{Completed}@5f2b9338
}

 

코드에서 잡의 상태를 확인하기 위해서는 isActive, isCompleted, isCancelled 프로퍼티를 사용하면 됩니다.

 

상태 isActive isCompleted isCancelled
New(지연 시작될 때 시작 상태) false false  false
Active(시작 상태 기본값) true false false
Completing(일시적인 상태) true false false
Cancelling(일시적안 상태) false false true
Cancelled(최종 상태) false true true
Completed(최종 상태) false true false

 

 

코루틴 빌더는 부모의 잡을 기초로 자신들의 잡을 생성한다

 

모든 코루틴 빌더는 자신만의 잡을 생성합니다.

launch는 Job을 반환하며, async는 Job 인터페이스를 구현하고 있는 Deferred<T> 타입을 반환합니다.

fun main(): Unit = runBlocking {
    val job: Job = launch { 
        delay(1000)
        println("Test")
    }
    val deferred: Deferred<String> = async { 
        delay(1000)
        "Test"
    }
    val deferredJob: Job = deferred
}

Job은 코루틴이 상속하지 않는 유일한 코루틴 컨텍스트이며, 모든 코루틴은 자신만의 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
    }
}

이처럼 잡은 부모-자식관계가 있기 때문에 코루틴 스코프 내에서 취소와 예외 처리 구현이 가능합니다.

fun main(): Unit = runBlocking {
    val job: Job = launch { 
        delay(1000)
    }
    val parentJob: Job = coroutineContext.job
    println(job == parentJob) // false
    val parentChildren: Sequence<Job> = parentJob.children
    println(parentChildren.first() == job) // true
}

새로운 Job 컨텍스트가 부모의 잡을 대체하면 구조화된 동시성의 작동 방식으로 유효하지 않습니다.

fun main(): Unit = runBlocking {
    launch(Job()) {
        delay(1000)
        println("Will not be printed")
    }
}
// 아무것도 출력되지 않고 종료됩니다.

위 예제에서, 자식은 인자로 들어온 잡을 부모로 사용하기 때문에 runBlocking과는 아무런 관련이 없게 됩니다.

부모-자식 관계가 없으면 구조화된 동시성을 잃게 됩니다.

 

 

자식들 기다리기 

 

잡은 코루틴이 완료될 때까지 기다릴 수 있게 하는 join 메서드가 있습니다.

join은 지정한 잡이 Completed나 Cancelled와 같은 마지막 상태에 도달할 때까지 기다리는 중단 함수입니다.

잡 인터페이스는 모든 자식을 참조할 수 있는 children 프로퍼티를 노출하며, 모든 자식이 마지막 상태가 될 때까지 기다리는데 활용할 수도 있습니다.

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

    val children = coroutineContext[Job]?.children
    val childNum = children?.count()
    println("Number of children: $childNum")
    children?.forEach { it.join() }
    println("All tests are done")
}
// Number of children: 2
// [1초 후]
// Test1
// [1초 후]
// Test2
// All tests are done

 

 

잡 팩토리 함수

 

코루틴을 생성할 때마다 Job 객체가 자동으로 생성되는데, 이것은 기본적으로 내부적으로 처리됩니다.

그러나 때때로 코루틴이 아닌 독립적인 작업을 추적하고 관리해야 할 때가 있습니다.

이럴 때에는 Job() 팩토리 함수를 사용하여 직접 Job 객체를 만들 수 있습니다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Test1")
    }
    launch(job) {
        delay(2000)
        println("Test2")
    }

    // job.join() 여기서 영원히 대기합니다.
    job.children.forEach { it.join() }
    println("All tests are done")
}
// [1초 후]
// Test1
// [1초 후]
// Test2
// All tests are done

Job()은 Job의 생성자를 호출한다고 생각할 수 있지만, Job은 인터페이스이며 인터페이스는 생성자를 갖지 못합니다.

팩토리 함수가 반환하는 실제 타입은 Job이 아니라 하위 인터페이스인 CompletableJob 입니다.

public fun Job(parent: Job? = null): CompletableJob

CompletableJob 인터페이스는 다음 두 가지 메서드를 추가하여 Job 인터페이스의 기능성을 확장했습니다.

 

  • complete(): Boolean - 잡을 완료하는 데 사용된다. complete 메서드를 사용하면 모든 자식 코루틴은 작업이 완료될 때까지 실행된 상태를 유지하지만, complete를 호출한 잡에서 새로운 코루틴이 시작될 수는 없다. 잡이 완료되면 실행 결과는 true가 되고, 그렇지 않을 경우 false가 됩니다.
  • completeExceptionally(exception: Throwable): Boolean - 인자로 받는 예외로 잡을 완료시킨다. 모든 자식 코루틴은 주어진 예외를 래핑한 CancellationException으로 즉시 취소된다. 반환값은 complete 메서드와 같다.

complete 함수는 잡의 마지막 코루틴을 시작한 후 자주 사용됩니다.

이후에는 join 함수를 사용해 잡이 완료되는 걸 기다리기만 하면 됩니다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Test1")
    }
    launch(job) {
        delay(2000)
        println("Test2")
    }

    job.complete()
    job.join()
    println("All tests are done")
}
// [1초 후]
// Test1
// [1초 후]
// Test2
// All tests are done

Job 함수의 인자로 부모 잡의 참조값을 전달할 수 있으며, 이때 부모 잡이 취소되면 해당 잡 또한 취소됩니다.

suspend fun main(): Unit = coroutineScope {
    val parentJob = Job()
    val job = Job(parentJob)
    launch(job) {
        delay(1000)
        println("Test1")
    }
    launch(job) {
        delay(2000)
        println("Test2")
    }
    delay(1100)
    job.cancel()
    job.children.forEach { it.join() }
}
// Test1

 

반응형