티스토리 뷰

728x90

 

 

지난 포스팅에서 구조화된 동시성의 특성에 대해 배웠다.

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

구조화된 동시성의 특성은 Job 컨텍스트와 관련이 있다.

Job은 코루틴을 취소하고, 상태를 파악하는 등 다양하게 사용될 수 있다.

 

 

Job이란 무엇인가?

잡은 수명을 가지고 있고 취소 가능하다.

Job은 인터페이스지만 구체적인 사용법과 상태를 가지고 있기 때문에 추상 클래스처럼 다룰 수도 있다.

 

잡의 수명은 상태로 나타낸다. 위 그림은 잡의 상태와 상태 변화를 나타낸 도식도이다.

  • Active
    • Active 상태에서는 잡이 실행되고 코루틴은 잡을 수행한다.
    • 잡이 코루틴 더로 생성되었을 코루틴의 본체가 실행되는 상태다.
    • 상태에서 자식 코루틴을 시작할 수 있고, 대부분의 코루틴은 Active 상태로 시작한다.
  • New
    • 지연 시작되는 코루틴만 New 상태에서 시작한다.
    • New 상태인 코루틴이 Active 상태가 되려면 작업이 실행되어야 한다. 코루틴이 본체를 실행하면 Active 상태로 가게 된다.
  • Completing
    • 실행이 완료되면 상태는 Completing으로 바뀌고 자식들을 기다린다.
  • Completed
    • 자식들의 실행까지 모두 끝났다면 잡은 마지막 상태인 Completed로 바뀐다.
  • Cancelling
    • 만약 잡이 실행 도중에(Active 또는 Completing 상태)취소되거나 실패하게 되면 Cancelling 상태로 가게 된다.
    • 여기서 연결을 끊거나 자원을 반납하는 등의 후처작업을 한다.
  • Cancelled
    • 후처작업이 완료되면 Cancelled 상태가 된다.

 

suspend fun main() = coroutineScope {
    val job = Job()
    println(job) // JobImpl{Active}@ADD
    // Job 빌더로 생성된 잡은 메서드로 완료시킬 때까지 Active 상태
    job.complete()
    println(job) // JobImpl{Completed}@ADD

    // launch는 기본적으로 Active 상태
    val activeJob = launch {
        delay(1000)
    }
    println(activeJob) // StandaloneCoroutine{Active}@ADD
    // 잡이 완료될 때까지 기다린다.
    activeJob.join() // (1초 후)
    println(activeJob) // StandaloneCoroutine{Completed}@ADD

    // New 상태로 지연 시작된다.
    val lazyJob = launch(start = CoroutineStart.LAZY) {
        delay(1000)
    }
    println(lazyJob) // LazyStandaloneCoroutine{New}@ADD
    // Active 상태가 되려면 잡을 시작하는 함수를 호출해야 한다.
    lazyJob.start()
    println(lazyJob) // LazyStandaloneCoroutine{Active}@ADD
    lazyJob.join() // (1초 후)
    println(lazyJob) // LazyStandaloneCoroutine{Completed}@ADD
}

위 코드는 Job의 여러 상태를 보여주고 있다. 잡의 상태는 toString() 함수로 볼 수 있다.

상태가 바뀔 때 출력되는 잡 또한 다르다는 걸 확인할 수 있다.

마지막 잡은 지연 시작되기 문에 start()를 호출하기 전까지 New 상태다. 다른 모든 잡은 생성되는 즉시 Active 상태가 된다.

 

상태 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

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

 

 

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

코루틴 라이브러리의 모든 코루틴 빌더는 자신만의 잡을 생성한다.

대부분의 코루틴 빌더는 잡을 반환하므로 어느 곳에서든 사용할 수 있다.

fun main(): Unit = runBlocking {
    val job: Job = launch {
        delay(1000)
        println("TEST")
    }
}

위 코드는 launch명시적 반환 타입이 Job임을 보여준다.

 

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

async 함수의 반환 타입은 Deferred<T>이다. Deferred<T>은 Job 인터페이스를 구현하고 있다.

 

val CoroutineContext.job: Job
    get() = get(Job) ?: error("Current context doesn't...")

fun main(): Unit = runBlocking {
    print(coroutineContext.job.isActive) // true
}

Job은 코루틴 컨텍스트이므로 coroutinecontext[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
    }
}

Job은 자식 코루틴이 부모 코루틴으로부터 상속받지 않는 유일한 코루틴 컨텍스트다. 이는 코루틴에서 아주 중요하다.

모든 코루틴은 자신만의 Job을 생성한다. 인자나 부모 코루틴으로부터 온 잡은 새로운 잡의 부모로 사용된다.

 

fun main(): Unit = runBlocking {
    val job: Job = launch {
        delay(1000)
    }

    val parentJob: Job = coroutineContext.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")
    }
}

 

위 코드는 아무것도 출력하지 않고, 즉시 종료된다.

코루틴이 자신만의 독자적인 잡을 가지면 부모와 아무런 관계가 없다.

 

launch는 인자로 들어온 새로운 잡이 부모가 되기 때문에 runBlocking과는 아무런 관련이 없는 것이다.

자식은 다른 컨텍스트들을 상속받지만, 부모-자식 관계는 정립되지 않는다.

부모-자식 관계가 없으면 구조화된 동시성을 잃게 되어 코루틴을 다룰 골치 아픈 상황이 발생한다.

 

 

자식들 기다리기

fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000)
        println("Test1")
    }
    val job2 = launch {
        delay(2000)
        println("Test2")
    }
    job1.join()
    job2.join()
    println("All tests are done")
}
(1초 후)
Test1
(1초 후)
Test2
All tests are done

잡의 중요한 이점은 코루틴이 완료될 때까지 기다리는 사용될 수 있다는 점이다. 이를 위해 join() 함수를 사용한다.

join()잡이 CompletedCancelled와 같은 마지막 상태가 될 때까지 기다리는 중단 함수다.

 

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

    val children = coroutineContext[Job]
        ?.children

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

Job 인터페이스에는 모든 자식을 참조할 수 있는 children 프로퍼티가 있다.

모든 자식이 끝날 때까지 기다리는 활용할 수 있다.

 

 

잡 팩토리 함수

JobJob() 팩토리 함수를 사용하면 코루틴 없이도 Job을 만들 수 있다.

팩토리 함수로 생성된 잡은 어떤 코루틴과도 연관되지 않고, 컨텍스트로 사용될 수 있다.

즉, 자식 코루틴을 가진 부모 잡으로 사용할 수도 있다.

 

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() 팩토리 함수를 사용해 잡을 생성하고, 다른 코루틴의 부모로 지정한 join()을 호출하는 것이다.

자식 코루틴모두 작업을 끝마쳐도 Job여전히 Active 상태기 때문에 프로그램이 종료되지 않는다.

팩토리 함수로 만들어진 잡은 다른 코루틴에서도 여전히 사용될 수 있기 때문이다.

 

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

따라서 잡의 모든 자식 코루틴의 join()을 호출해야 한다.

 

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

Job() 팩토리 함수를 처음보면 Job생성자를 호출한다고 각할 수 있다.

하지만 Job은 인터페이스기 때문에 생성자를 갖지 못한다.

Job()은 생성자처럼 보이는 함수로, 가짜 생성자다.

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

 

CompletableJob 인터페이스는 다음 두 함수를 추가해 Job 인터페이스의 기능을 확장했다.

  • complete(): Boolean
  • completeExceptionally(exception: Throwable): Boolean

 

fun main() = runBlocking {
    val job = Job()
    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("Rep$num")
        }
    }
    launch {
        delay(500)
        job.complete()
    }

    job.join()
    launch(job) {
        println("Will not be printed")
    }
    println("Done")
}
Rep0
Rep1
Rep2
Rep3
Rep4
Done

complete() 함수는 잡을 완료하는 사용된다.

complete() 함수를 호출하면 모든 자식 코루틴은 잡이 완료될 때까지 Active 상태를 유지한다.

complete()를 호출한 후 해당 잡을 통해 새로운 코루틴을 시작할 수는 없다.

잡이 성공적으로 완료되면 true를 반환하고, 그렇지 않을 경우 false를 반환한다. (예를 들어 잡이 이미 완료된 경우)

 

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }
    launch(job) {
        delay(2000)
        println("Text 2")
    }
    job.complete()
    job.join()
}
(1초 후)
Text 1
(1초 후)
Text 2

일반적으로 complete() 함수는 잡의 마지막 코루틴을 시작한 후 사용한다.

이후에는 join() 함수를 사용해 잡이 완료되는  기다리기만 하면 된다.

 

fun main() = runBlocking {
    val job = Job()
    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("Rep$num")
        }
    }
    launch {
        delay(500)
        job.completeExceptionally(Error("Some error"))
    }

    job.join()
    launch(job) {
        println("Will not be printed")
    }
    println("Done")
}

completeExceptionally() 함수는 인자로 받은 예외로 잡을 완료시킨다.

모든 자식 코루틴은 인자로 받은 예외를 래핑한 CancellationException으로 즉시 취소된다.

반환값은 complete() 함수와 동일하다. 즉, "잡이 메서드의 실행으로 종료되었습니까?"라는 질문에 대한 응답이 된다.

 

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

Job() 팩토리 함수의 인자로 부모 잡을 전달할 수도 있다.이 때 부모 잡이 취소되면 해당 잡도 취소된다.

 

 

 

출처

https://www.yes24.com/product/goods/123034354

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
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
글 보관함