관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 8. Job, 자식 코루틴 기다리기 본문

책/코틀린 코루틴

[코틀린 코루틴] 8. Job, 자식 코루틴 기다리기

참깨빵위에참깨빵 2024. 2. 17. 23:04
728x90
반응형

구조화된 동시성의 중요 특징 3개는 Job 컨텍스트와 관련 있다. Job은 코루틴 취소, 상태 파악 등 여러 곳에서 다양하게 쓰일 수 있다.

 

Job이란

 

Job은 수명을 갖고 있고 취소할 수 있다. Job은 인터페이스지만 구체적 사용법, 상태를 갖고 있어서 추상 클래스처럼 다룰 수 있다. 아래는 Job의 수명을 나타낸 그림이다.

 

 

Active 상태에선 Job이 실행되고 코루틴은 Job을 수행한다. Job이 코루틴 빌더에 의해 생성됐을 때 코루틴 본체가 실행되는 상태다. 이 때 자식 코루틴을 실행할 수 있고 대부분의 코루틴은 Active 상태로 시작한다.

지연 시작되는 코루틴만 New 상태에서 시작한다. New 상태인 코루틴이 Active 상태가 되려면 작업이 실행돼야 한다. 코루틴이 본체를 실행하면 Active 상태로 이동한다. 실행이 완료되면 상태는 Completing으로 바뀌고 자식들을 기다린다. 자식들도 실행이 완료되면 Job은 마지막 상태인 Completed로 바뀐다.

Job 실행 중 취소되거나 실패하면 Cancelling 상태로 바뀐다. 여기서 연결 끊기, 자원 반납 등 후처리를 할 수 있다. 후처리까지 끝나면 Job은 Cancelled 상태가 된다.

아래는 Job의 상태를 확인하는 예시다.

 

import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

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

    // launch는 기본적으로 활성화돼 있다
    val activeJob = launch {
        delay(1000)
    }
    println(activeJob)  // StandaloneCoroutine{Active}@49fc609f

    // 여기서 Job의 완료를 기다린다
    activeJob.join()    // (1초 후)
    println(activeJob)  // StandaloneCoroutine{Completed}@49fc609f

    // launch는 New 상태로 지연 시작된다
    val lazyJob = launch(start = CoroutineStart.LAZY) {
        delay(1000)
    }
    println(lazyJob)    // LazyStandaloneCoroutine{New}@51b7e6e0

    // Active 상태가 되려면 시작하는 함수를 호출해야 한다
    lazyJob.start()
    println(lazyJob)    // LazyStandaloneCoroutine{Active}@51b7e6e0

    lazyJob.join()  // (1초 후)
    println(lazyJob)    // LazyStandaloneCoroutine{Completed}@51b7e6e0
}

 

마지막 Job은 지연처리되서 저절로 시작하지 않는다. 다른 모든 Job은 생성 즉시 Active 상태가 된다. 코드에서 Job의 상태를 확인하려면 isActive, isCompleted, isCancelled 프로퍼티를 사용한다.

 

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

 

모든 코루틴 빌더는 자신만의 Job을 생성한다. 대부분의 코루틴 빌더는 Job을 리턴하므로 어디서든 쓸 수 있다.

또한 Job은 코루틴 컨텍스트기 때문에 coroutineContext[Job]을 써서 접근할 수 있다. 더 접근하기 편하게 하는 확장 프로퍼티 job도 있다.

 

import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.CoroutineContext

// 확장 프로퍼티
val CoroutineContext.job: Job
    get() = get(Job) ?: error("이 context는 Job을 갖고 있지 않습니다")

fun main(): Unit = runBlocking {
    println(coroutineContext.job.isActive)
}

// true

 

Job은 코루틴이 상속하지 않는 유일한 코루틴 컨텍스트다. 모든 코루틴은 자신만의 Job을 생성하고 인자 또는 부모 코루틴한테서 온 Job은 새 Job의 부모로 쓰인다.

 

자식들 기다리기

 

Job의 중요한 첫 번째 이점은 코루틴이 완료될 때까지 기다리는 데 쓰일 수 있단 것이다. 이걸 위해 join()을 쓴다.

join()은 지정한 Job이 Completed, Cancelled 같은 마지막 상태에 도달할 때까지 기다리는 중단 함수다.

Job은 모든 자식을 참조할 수 있는 children 프로퍼티도 제공해서 모든 자식이 마지막 상태가 될 때까지 기다리는 데 활용할 수 있다.

 

import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

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

    val children = coroutineContext[Job]?.children
    val childrenNum = children?.count()
    println("children 개수 : $childrenNum")

    children?.forEach {
        it.join()
    }
    println("모든 테스트 종료")
}

// children 개수 : 2
// (1초 후)
// test1
// (1초 후)
// test2
// 모든 테스트 종료

 

Job 팩토리 함수

 

Job은 Job() 팩토리 함수를 쓰면 코루틴 없이도 Job을 만들 수 있다. 흔한 실수 중 하나는 Job() 팩토리 함수로 Job을 만들고 다른 코루틴의 부모로 지정한 뒤 join()을 쓰는 것이다. 이렇게 되면 자식 코루틴들이 모두 작업을 끝내도 Job은 여전히 Active 상태에 있기 때문에 프로그램이 종료되지 않는다. 따라서 Job의 모든 자식 코루틴에서 join()을 호출하는 게 바람직하다.

 

import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {   // 새 Job이 부모로부터 상속받은 Job을 대체
        delay(1000)
        println("test1")
    }
    launch(job) {   // 새 Job이 부모로부터 상속받은 Job을 대체
        delay(2000)
        println("test2")
    }

    job.children.forEach {
        it.join()
    }
}

// (1초 후)
// test1
// (1초 후)
// test2

 

Job()은 Job의 생성자를 호출하는 걸로 볼 수 있지만 Job은 인터페이스라서 생성자를 가질 수 없다. Job()은 생성자처럼 보이는 간단한 함수로 가짜 생성자다. 이 팩토리 함수가 리턴하는 실제 타입은 하위 인터페이스인 CompletableJob이다.

 

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

 

CompletableJob

A job that can be completed using complete() function. It is returned by Job() and SupervisorJob() constructor functions. All functions on this interface are thread-safe and can be safely invoked from concurrent coroutines without external synchronization.

kotlinlang.org

complete()를 써서 완료할 수 있는 Job이다. Job(), SupervisorJob() 함수에 의해 리턴된다. 이 인터페이스의 모든 함수는 쓰레드 세이프하며 외부 동기화 없이 동시 코루틴(concurrent coroutine)에서 안전하게 호출할 수 있다. 향후 CompletableJob 인터페이스에 새 메서드가 추가될 수 있으므로 서드파티 라이브러리에서 상속하기엔 안정적이지 않지만 쓰기에는 안정적이다

 

CompletableJob 인터페이스는 아래 2가지 메서드를 추가해서 Job 인터페이스의 기능을 확장했다.

 

  1. Boolean complete() : Job()을 완료하는 데 쓰인다. 이 메서드를 쓰면 모든 자식 코루틴은 작업이 완료될 때까지 실행 상태를 유지하지만 complete()를 호출한 Job에서 새 코루틴이 시작될 수는 없다. Job이 완료되면 실행 결과는 true가 되고 그렇지 않으면 false가 된다.
  2. Boolean completeExceptionally(exception: Throwable) : 인자로 받은 예외로 Job을 완료시킨다. 모든 자식 코루틴은 주어진 예외를 래핑한 CancellationException으로 즉시 취소된다. complete()처럼 리턴값은 Job이 메서드의 실행으로 종료됐는가? 에 대한 응답이다.

 

complete()는 Job의 마지막 코루틴을 시작한 후 자주 쓰인다. 이후엔 join()을 써서 Job이 완료되는 걸 기다리면 된다.

 

import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(): Unit = coroutineScope {
    val job = Job()

    launch(job) {
        delay(1000)
        println("test1")
    }

    launch(job) {
        delay(2000)
        println("test2")
    }

    job.complete()
    job.join()
}

// (1초 후)
// test1
// (1초 후)
// test2

 

반응형
Comments