반응형
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
관리 메뉴

안드로이드 개발자 노트

[코루틴의 정석] 7장. 구조화된 동시성 본문

Kotlin/코루틴의 정석

[코루틴의 정석] 7장. 구조화된 동시성

어리둥절범고래 2024. 9. 8. 13:47
반응형

구조화된 코루틴의 대표적인 특징은 다음과 같다.

 

  • 부모 코루틴의 실행 환경이 자식 코루틴에게 상속된다.
  • 작업을 제어하는 데 사용된다.
  • 부모 코루틴이 취소되면 자식 코루틴도 취소된다.
  • 부모 코루틴은 자식 코루틴이 완료될 때까지 대기한다.
  • CoroutineScope를 사용해 코루틴이 실행되는 범위를 제한할 수 있다.
fun main() = runBlocking<Unit> {
  launch { // 부모 코루틴
    launch {  // 자식 코루틴
      println("자식 코루틴 실행")
    }
  }
}

 


7.1. 실행 환경 상속

 

7.1.1. 부모 코루틴의 실행 환경 상속

 

부모 코루틴이 자식 코루틴을 생성하면 부모 코루틴이 CoroutineContext가 자식 코루틴에게 전달된다.

fun main() = runBlocking<Unit> {
  val coroutineContext = newSingleThreadContext("MyThread") + CoroutineName("CoroutineA")
  launch(coroutineContext){ // 부모 코루틴 생성
    println("[${Thread.currentThread().name}] 부모 코루틴 실행")
    launch {  // 자식 코루틴 생성
      println("[${Thread.currentThread().name}] 자식 코루틴 실행")
    }
  }
}
/*
// 결과:
[MyThread @CoroutineA#2] 부모 코루틴 실행
[MyThread @CoroutineA#3] 자식 코루틴 실행
*/

 


7.1.2. 실행 환경 덮어씌우기

 

자식 코루틴을 생성하는 코루틴 빌더 함수로 새로운 CoroutineContext 객체가 전달되면 전달된 CoroutineContext 객체의 구성 요소들로 덮어씌워진다.

fun main() = runBlocking<Unit> {
  val coroutineContext = newSingleThreadContext("MyThread") + CoroutineName("ParentCoroutine")
  launch(coroutineContext){ // 부모 코루틴 생성
    println("[${Thread.currentThread().name}] 부모 코루틴 실행")
    launch(CoroutineName("ChildCoroutine")) {  // 자식 코루틴 생성
      println("[${Thread.currentThread().name}] 자식 코루틴 실행")
    }
  }
}
/*
// 결과:
[MyThread @ParentCoroutine#2] 부모 코루틴 실행
[MyThread @ChildCoroutine#3] 자식 코루틴 실행
*/

 

 


7.1.3. 상속되지 않는 Job

 

다른 CoroutineContext 구성 요소들과 다르게 Job 객체는 상속되지 않고 코루틴 빌더 함수가 호출될 때마다 새롭게 생성된다.

fun main() = runBlocking<Unit> { // 부모 코루틴 생성
  val runBlockingJob = coroutineContext[Job] // 부모 코루틴의 CoroutineContext로부터 부모 코루틴의 Job 추출
  launch { // 자식 코루틴 생성
    val launchJob = coroutineContext[Job] // 자식 코루틴의 CoroutineContext로부터 자식 코루틴의 Job 추출
    if (runBlockingJob === launchJob) {
      println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 동일합니다")
    } else {
      println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다")
    }
  }
}
/*
// 결과:
runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다
*/

 

 

7.1.4. 구조화에 사용되는 Job

 

코루틴 빌더가 호출되면 Job 객체는 새롭게 생성되지만 생성된 Job 객체는 내부에 정의된 parent 프로퍼티를통해 부모 코루틴의 Job 객체에 대한 참조를 가진다. 또한 부모 코루틴의 Job 객체는 Sequence 타입의 children 프로퍼티를 통해 자식 코루틴의 Job에 대한 참조를 가져 부모와 자식 코루틴은 양방향 참조를 가지게 된다.

public interface Job : CoroutineContext.Element {

    public companion object Key : CoroutineContext.Key<Job>

    public val parent: Job?
    
    public val children: Sequence<Job>
    
    ...
}

 

 

Job 프로퍼티 타입 설명
parent Job? 코루틴은 부모 코루틴이 없을 수 있고(루트 코루틴), 부모 코루틴이 있더라도 최대 하나이다.
children Sequence<Job> 하나의 코루틴이 복수의 자식 코루틴을 가질 수 있다.
fun main() = runBlocking<Unit> { // 부모 코루틴
  val parentJob = coroutineContext[Job] // 부모 코루틴의 CoroutineContext로부터 부모 코루틴의 Job 추출
  launch { // 자식 코루틴
    val childJob = coroutineContext[Job] // 자식 코루틴의 CoroutineContext로부터 자식 코루틴의 Job 추출
    println("1. 부모 코루틴과 자식 코루틴의 Job은 같은가? ${parentJob === childJob}")
    println("2. 자식 코루틴의 Job이 가지고 있는 parent는 부모 코루틴의 Job인가? ${childJob?.parent === parentJob}")
    println("3. 부모 코루틴의 Job은 자식 코루틴의 Job을 참조를 가지는가? ${parentJob?.children?.contains(childJob)}")
  }
}
/*
// 결과:
1. 부모 코루틴과 자식 코루틴의 Job은 같은가? false
2. 자식 코루틴의 Job이 가지고 있는 parent는 부모 코루틴의 Job인가? true
3. 부모 코루틴의 Job은 자식 코루틴의 Job을 참조를 가지는가? true
*/

 

 


7.2. 코루틴의 구조화와 작업 제어

 

코루틴 구조화는 하나의 큰 비동기 작업을 작은 비동기 작업으로 나누면서 생긴다.

 

이런 식으로 코루틴을 구조화 하는 이유는 코루틴을 안전하게 관리하고 제어하기 위함이다.

구조화된 코루틴은 안전하게 제어되기 위한 몇 가지 특성을 가진다.

 

  1. 코루틴으로 취소가 요청되면 자식 코루틴으로 전파한다.
  2. 부모 코루틴은 모든 자식 코루틴이 실행 완료돼야 완료될 수 있다.



7.2.1. 취소의 전파

 

코루틴은 자식 코루틴으로 취소를 전파하는 특성을 가진다.

 


특정 코루틴에 취소가 요청되면 자식 코루틴 방향으로 전파되며, 부모 코루틴으로는 전파되지 않는다.

 

fun main() = runBlocking<Unit> {
  val parentJob = launch(Dispatchers.IO){ // 부모 코루틴 생성
    val dbResultsDeferred: List<Deferred<String>> = listOf("db1","db2","db3").map {
      async { // 자식 코루틴 생성
        delay(1000L) // DB로부터 데이터를 가져오는데 걸리는 시간
        println("${it}으로부터 데이터를 가져오는데 성공했습니다")
        return@async "[${it}]data"
      }
    }
    val dbResults: List<String> = dbResultsDeferred.awaitAll() // 모든 코루틴이 완료될 때까지 대기

    println(dbResults) // 화면에 표시
  }
  parentJob.cancel() // 부모 코루틴에 취소 요청
}
/*
// 결과: 출력되지 않음
*/



7.2.2. 부모 코루틴의 자식 코루틴에 대한 완료 의존성

 

부모 코루틴은 모든 자식 코루틴이 실행 완료되어야 완료될 수 있으며, 이를 부모 코루틴이 자식 코루틴에 대해 완료 의존성을 가진다고 한다.

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val parentJob = launch { // 부모 코루틴 실행
    launch { // 자식 코루틴 실행
      delay(1000L) // 1초간 대기
      println("[${getElapsedTime(startTime)}] 자식 코루틴 실행 완료")
    }
    println("[${getElapsedTime(startTime)}] 부모 코루틴이 실행하는 마지막 코드")
  }
  parentJob.invokeOnCompletion { // 부모 코루틴이 종료될 시 호출되는 콜백 등록
    println("[${getElapsedTime(startTime)}] 부모 코루틴 실행 완료")
  }
}
/*
// 결과:
[지난 시간: 3ms] 부모 코루틴이 실행하는 마지막 코드
[지난 시간: 1019ms] 자식 코루틴 실행 완료
[지난 시간: 1019ms] 부모 코루틴 실행 완료
*/

invokeOnCompletion 콜백은 코루틴이 실행 완료 혹은 취소 완료된 경우 동작한다.

 


7.2.2.1 실행 완료 중 상태

 

실행 완료 중 상태란 부모 코루틴의 모든 코드가 실행됐지만 자식 코루틴이 실행 중인 경우 부모 코루틴이 갖는 상태를 말한다.



7.2.2.2 실행 완료 중 상태의 Job 상태 값

 

'실행 완료 중' 상태는 '실행 중' 상태와 구분 없이 사용한다.

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val parentJob = launch { // 부모 코루틴 생성
    launch { // 자식 코루틴 생성
      delay(1000L) // 1초간 대기
      println("[${getElapsedTime(startTime)}] 자식 코루틴 실행 완료")
    }
    println("[${getElapsedTime(startTime)}] 부모 코루틴이 실행하는 마지막 코드")
  }
  parentJob.invokeOnCompletion { // 부모 코루틴이 종료될 시 호출되는 콜백 등록
    println("[${getElapsedTime(startTime)}] 부모 코루틴 실행 완료")
  }
  delay(500L) // 500밀리초간 대기
  printJobState(parentJob) // parentJob 상태 출력
}
/*
// 결과:
[지난 시간: 6ms] 부모 코루틴이 실행하는 마지막 코드
Job State
isActive >> true
isCancelled >> false
isCompleted >> false
[지난 시간: 1023ms] 자식 코루틴 실행 완료
[지난 시간: 1023ms] 부모 코루틴 실행 완료
*/

'실행 완료 중' 상태와 '실행 중' 상태의 상태 값은 구분되지 않지만, 자식 코루틴이 실행 완료되지 않으면 부모 코루틴도 실행 완료될 수 없다는 점에서 구분할 필요가 있다.

코루틴 상태 isActive isCancelled isCompleted
생성 false false false
실행 중 true false false
실행 완료 중 true false false
실행 완료 false false true
취소 중 false true false
취소 완료 false true true

 


7.3. CoroutineScope 사용해 코루틴 관리하기

 

7.3.1. CoroutineScope 생성하기

 

7.3.1.1. CoroutineScope 인터페이스 구현을 통한 생성

 

CoroutineScope 인터페이스는 코루틴의 실행 환경인 CoroutineContext를 가진 단순한 인터페이스다.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

이 인터페이스를 구현한 구체적인 클래스(Concrete Class)를 사용하면 CoroutineScope 객체를 생성할 수 있다.

class CustomCoroutineScope : CoroutineScope {
  override val coroutineContext: CoroutineContext = Job() +
      newSingleThreadContext("CustomScopeThread")
}

fun main() {
  val coroutineScope = CustomCoroutineScope() // CustomCoroutineScope 인스턴스화
  coroutineScope.launch {
    delay(100L) // 100밀리초 대기
    println("[${Thread.currentThread().name}] 코루틴 실행 완료")
  }
  Thread.sleep(1000L) // 코드 종료 방지
}

/*
// 결과:
[CustomScopeThread @coroutine#1] 코루틴 실행 완료
*/

CustomCoroutineScope 객체로부터 코루틴 실행 환경을 제공받는 것을 확인할 수 있다.

 


7.3.1.2. CoroutineScope 함수를 사용해 생성

 

코틀린은 CoroutineScope 객체를 생성하는 함수를 제공한다.

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
    // Job이 설정돼 있지 않으면 새로운 Job 생성

다음과 같이 사용할 수 있다.

fun main() {
  val coroutineScope = CoroutineScope(Dispatchers.IO)
  coroutineScope.launch {
    delay(100L) // 100밀리초 대기
    println("[${Thread.currentThread().name}] 코루틴 실행 완료")
  }
  Thread.sleep(1000L)
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#1] 코루틴 실행 완료
*/



7.3.2. 코루틴에게 실행 환경을 제공하는 CoroutineScope


7.3.2.1. CoroutineScope가 코루틴에게 실행 환경을 제공하는 방식

 

launch 코루틴 빌더 함수는 CoroutineScope의 확장 함수로 선언돼 있다.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

 launch 함수가 호출되면 다음 과정을 통해 CoroutineScope 객체로부터 실행 환경을 제공받아 코루틴의 실행 환경을 설정한다.

 

  1. 수신 객체인 CoroutineScope로부터 CoroutineContext 객체를 제공받는다.
  2. 제공받은 CoroutineContext 객체에 launch 함수의 context 인자로 넘어온 CoroutineContext를 더한다.
  3. 생성된 CoroutineContext에 코루틴 빌더 함수가 호출돼 새로 생성되는 Job을 더한다. 이때 CoroutineContext를 통해 전달되는 Job 객체는 새로 생성되는 Job 객체의 부모 Job 객체가 된다.
fun main() {
  val newScope = CoroutineScope(CoroutineName("MyCoroutine") + Dispatchers.IO)
  newScope.launch(CoroutineName("LaunchCoroutine")) {
    println(this.coroutineContext[CoroutineName])
    println(this.coroutineContext[CoroutineDispatcher])
    val launchJob = this.coroutineContext[Job]
    val newScopeJob = newScope.coroutineContext[Job]
    println("launchJob?.parent === newScopeJob >> ${launchJob?.parent === newScopeJob}")
  }
  Thread.sleep(1000L)
}
/*
// 결과:
CoroutineName(LaunchCoroutine)
Dispatchers.IO
launchJob?.parent === newScopeJob >> true
*/

 



7.3.2.2. CoroutineScope로부터 실행 환경 상속받기

 

코루틴 빌더를 통해 생성되는 코루틴의 CoroutineContext 객체는 코루틴 빌더 수신객체인 CoroutineScope를 통해 제공된다.

코루틴 빌더 함수의 람다식에서 모두 수신 객체로 CoroutineScope 객체를 제공하고 있으며, 이를 통해 코루틴의 실행 환경이 상속된다는 것을 알 수 있다.

 


7.3.3. CoroutineScope에 속한 코루틴의 범위


7.3.3.1. CoroutineScope에 속한 코루틴의 범위

 

CoroutineScope 객체는 기본적으로 특정 범위의 코루틴들을 제어하는 역할을 한다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    launch(CoroutineName("Coroutine3")) {
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) {
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }

  launch(CoroutineName("Coroutine2")) {
    println("[${Thread.currentThread().name}] 코루틴 실행")
  }
}

코루틴 빌더 람다식에서 수신 객체로 제공되는 CoroutineScope 객체는 코루틴 빌더로 생성되는 코루틴과 람다식 내에서 CoroutineScope 객체를 사용해 실행되는 모든 코루틴을 포함한다.

 


7.3.3.2. CoroutineScope를 새로 생성해 기존 CoroutineScope 범위에서 벗어나기

 

코루틴은 Job 객체를 사용해 구조화되는데 CoroutineScope 함수를 사용해 새로운 CoroutineScope 객체를 생성하면 기존의 계층 구조를 따르지 않는 새로운 Job 객체가 생성돼 새로운 계층 구조를 만들게 된다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    launch(CoroutineName("Coroutine3")) {
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    CoroutineScope(Dispatchers.IO).launch(CoroutineName("Coroutine4")) {
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }

  launch(CoroutineName("Coroutine2")) {
    println("[${Thread.currentThread().name}] 코루틴 실행")
  }
}

이렇게 새로운 계층 구조가 만들어지면 runBlocking과는 아무런 관련이 없어진다.

7.3.4. CoroutineScope 취소하기

 

CoroutineScope 인터페이스의 cancel 함수는 CoroutineScope 객체의 범위에 속한 모든 코루틴을 취소하는 함수다.

CoroutineScope 객체에 cancel 함수가 호출되면 범위에서 실행 중인 모든 코루틴에 취소가 요청된다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    launch(CoroutineName("Coroutine3")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행 완료")
    }
    launch(CoroutineName("Coroutine4")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행 완료")
    }
    this.cancel() // Coroutine1의 CoroutineScope에 cancel 요청
  }

  launch(CoroutineName("Coroutine2")) {
    delay(100L)
    println("[${Thread.currentThread().name}] 코루틴 실행 완료")
  }
}
/*
// 결과:
[main @Coroutine2#3] 코루틴 실행 완료
*/

 

다음은 cancel 함수의 구현체이다.

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

 

CoroutineScope 객체에 cancel 함수가 호출되면 CoroutineScope 객체는 자신의 coroutineContext 프로퍼티를 통해 Job 객체에 접근한 후 cancel 함수를 호출한다.

 


7.3.5. CoroutineScope 활성화 상태 확인하기

 

CoroutineScope 객체는 자신의 coroutineContext 프로퍼티를 통해 Job 객체의 isActive 프로퍼티를 확인한다.

public val CoroutineScope.isActive: Boolean
    get() = coroutineContext[Job]?.isActive ?: true

 

 


7.4. 구조화와 Job

 

7.4.1. runBlocking과 루트 Job

 

부모 Job 객체가 없는 구조화의 시작점 역할을 하는 Job 객체를 루트 Job이라고 한다.

이 Job 객체에 의해 제어되는 코루틴을 루트 코루틴이라고 한다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
    launch(CoroutineName("Coroutine3")) { // Coroutine3 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) { // Coroutine4 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
    launch(CoroutineName("Coroutine5")) { // Coroutine5 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  delay(1000L)
}

runBlocking 코루틴을 루트 코루틴으로 해서 하위에 모든 코루틴들이 구조화되는 것을 볼 수 있다.

 

 

 

7.4.2. Job 구조화 깨기


7.4.2.1. CoroutineScope 사용해 구조화 깨기

 

CoroutineScope 객체가 생성되면 새로운 루트 Job이 생성되며, 이를 사용해 코루틴의 구조화를 깰 수 있다.

fun main() = runBlocking<Unit> { // 루트 Job 생성
  val newScope = CoroutineScope(Dispatchers.IO) // 새로운 루트 Job 생성
  newScope.launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
    launch(CoroutineName("Coroutine3")) { // Coroutine3 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) { // Coroutine4 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  newScope.launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
    launch(CoroutineName("Coroutine5")) { // Coroutine5 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
}
/*
// 결과:
Process finished with exit code 0
*/

이 코드를 구조화하면 다음과 같다.

아무런 결과가 나오지 않는 이유는 구조화가 깨졌기 때문이다.

fun main() = runBlocking<Unit> {
  val newScope = CoroutineScope(Dispatchers.IO)
  newScope.launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
    launch(CoroutineName("Coroutine3")) { // Coroutine3 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) { // Coroutine4 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  newScope.launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
    launch(CoroutineName("Coroutine5")) { // Coroutine5 실행
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  delay(1000L) // 1초간 대기
}
/*
// 결과:
[DefaultDispatcher-worker-3 @Coroutine3#4] 코루틴 실행
[DefaultDispatcher-worker-8 @Coroutine5#6] 코루틴 실행
[DefaultDispatcher-worker-2 @Coroutine4#5] 코루틴 실행
*/

 

runBlocking 코루틴이 1초간 대기하면 newScope 하위 모든 코루틴이 실행돼 결과가 출력되는 것을 볼 수 있다.

 


7.4.2.1. CoroutineScope 사용해 구조화 깨기

 

Job()을 통해 부모가 없는 루트 Job을 생성할 수 있다.

fun main() = runBlocking<Unit> {
  val newRootJob = Job() // 루트 Job 생성
  launch(CoroutineName("Coroutine1") + newRootJob) {
    launch(CoroutineName("Coroutine3")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  launch(CoroutineName("Coroutine2") + newRootJob) {
    launch(CoroutineName("Coroutine5")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  delay(1000L)
}
/*
// 결과:
[main @Coroutine3#4] 코루틴 실행
[main @Coroutine4#5] 코루틴 실행
[main @Coroutine5#6] 코루틴 실행
*/

newRootJob.cancel()이 호출되면 하위 모든 Job 객체에 취소가 전파돼 코루틴이 취소된다.

fun main() = runBlocking<Unit> {
  val newRootJob = Job() // 새로운 루트 Job 생성
  launch(CoroutineName("Coroutine1") + newRootJob) {
    launch(CoroutineName("Coroutine3")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  launch(CoroutineName("Coroutine2") + newRootJob) {
    launch(CoroutineName("Coroutine5")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  newRootJob.cancel() // 새로운 루트 Job 취소
  delay(1000L)
}
/*
// 결과:
Process finished with exit code 0
*/

 


7.4.3. Job 사용해 일부 코루틴만 취소되지 않게 만들기

 

계층 구조를 끊음으로써 일부 코루틴만 취소되지 않도록 설정할 수 있다.

fun main() = runBlocking<Unit> {
  val newRootJob = Job() // 새로운 루트 Job 생성
  launch(CoroutineName("Coroutine1") + newRootJob) {
    launch(CoroutineName("Coroutine3")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    launch(CoroutineName("Coroutine4")) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  launch(CoroutineName("Coroutine2") + newRootJob) {
    launch(CoroutineName("Coroutine5") + Job()) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
  delay(50L) // 모든 코루틴이 생성될 때까지 대기
  newRootJob.cancel() // 새로운 루트 Job 취소
  delay(1000L)
}
/*
// 결과:
[main @Coroutine5#6] 코루틴 실행
*/

 

newRootJob에 취소가 요청되더라도 새로운 루트 Job의 자식이 돼 버린 Coroutine5에는 취소가 전파되지 않는다.



7.4.4. 생성된 Job의 부모를 명시적으로 설정하기

 

Job()을 통해 Job 객체를 생성할 경우 parent 프로퍼티가 null이 돼 부모가 없는 루트 Job이 생성된다.

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

parent 인자로 Job 객체를 넘기면 해당 Job을 부모로 하는 새로운 Job객체를 생성할 수 있다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    val coroutine1Job = this.coroutineContext[Job] // Coroutine1의 Job
    val newJob = Job(parent = coroutine1Job)
    launch(CoroutineName("Coroutine2") + newJob) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
  }
}
/*
// 결과:
[main @Coroutine2#3] 코루틴 실행
// 프로세스 종료 로그가 출력되지 않는다.
*/



7.4.5. 생성된 Job은 자동으로 실행 완료되지 않는다.

 

Job 생성 함수를 통해 생성된 Job 객체는 자식 코루틴들이 모두 실행 완료되더라도 자동으로 실행 완료되지 않으며, 명시적으로 완료 함수인 complete을 호출해야 완료된다.

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    val coroutine1Job = this.coroutineContext[Job]
    val newJob = Job(coroutine1Job)
    launch(CoroutineName("Coroutine2") + newJob) {
      delay(100L)
      println("[${Thread.currentThread().name}] 코루틴 실행")
    }
    newJob.complete() // 명시적으로 완료 호출
  }
}
/*
// 결과:
[main @Coroutine2#3] 코루틴 실행

Process finished with exit code 0
*/

 


요약

 

  1. 구조화된 동시성의 원칙이란 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적이고 예측할 수 있게 만드는 원칙이다
  2. 코루틴은 구조화된 동시성의 원칙을 통해 코루틴을 부모-자식 관계로 구조화 해 안정적인 비동기 프로그래밍이 가능하게 한다.
  3. 부모 코루틴은 자식 코루틴에게 실행 환경을 상속한다.
  4. 코루틴 빌더 함수에 전달된 CoroutineContext 객체를 통해 부모 코루틴의 실행 환경 중 일부 또는 전부를 덮어쓸 수 있다.
  5. 코루틴 빌더가 호출될 때마다 코루틴 제어를 위해 새로운 Job 객체가 생성된다.
  6. Job 객체는 부모 코루틴의 Job 객체를 Job? 타입의 parent 프로퍼티를 통해 전달된다.
  7. parent 프로퍼티가 null인 Job 객체는 구조화의 시작점 역할을 하는 루트 Job 객체이다.
  8. Job 객체는 자식 Job 객체들을 Sequence<Job> 타입의 children 프로퍼티를 통해 참조한다.
  9. Job 객체는 코루틴의 구조화에 핵심적인 역할을 한다.
  10. 부모 코루틴은 자식 코루틴이 완료될 때까지 완료되지 않는다. 만약 부모 코루 틴이 실행해야 할 코드가 모두 실행됐는데 자식 코루틴이 실행 중이라면 부모 코루틴은 '실행 완료 중' 상태를 가진다
  11. 부모 코루틴이 취소되면 취소가 모든 자식 코루틴으로 전파된다. 하지만 자식 코루틴의 취소는 부모 코루틴으로 전파되지 않는다.
  12. CoroutineScope 객체를 사용해 코루틴의 실행 범위를 제어할 수 있다.
  13. CoroutineScope 인터페이스는 코루틴 실행 환경인 CoroutineContext를 가진 인터페이스로 확장 함수로 launch, async 등의 함수를 가진다.
  14. launch나 async가 호출되면 CoroutineScope 객체로부터 실행 환경을 제공 받아 코루틴이 실행된다.
  15. CoroutineScope 객체에 대해 cancel 함수를 호출해 CoroutineScope 객체의 범위에 속한 모든 코루틴을 취소할 수 있다.
  16. CoroutineScope 객체에 대해 cancel 함수를 호출하는 것은 CoroutineScope 객체가 가진 CoroutineContext 객체의 Job 객체에 대해 cancel 함수를 호출하는 것이다.
  17. CoroutineScope 객체의 활성화 상태를 isActive 확장 프로퍼티를 통해 확인할 수 있다.
  18. CoroutineScope의 isActive 확장 프로퍼티는 CoroutineScope 객체가 가 진 CoroutineContext 객체의 Job 객체에 대한 isActive 프로퍼티를 확인하는 것이다.
  19. 별도의 범위를 갖는 CoroutineScope 객체를 생성해 코루틴의 구조화를 깰 수 있다.
  20. Job 생성 함수를 호출해 Job 객체를 생성할 수 있으며, 이를 사용해 코루틴의 구조화를 깨거나 유지할 수 있다.
  21. Job 생성 함수를 통해 생성된 Job 객체는 자동으로 실행 완료되지 않으므로 Job 객체에 대해 complete 함수를 호출해 명시적으로 완료 처리해야 한다.
  22. runBlocking 코루틴 빌더는 생성된 코루틴이 완료될 때까지 호출 스레드를 차단하고 사용하는 코루틴을 만드는 반면에 launch 코루틴 빌더로 생성된 코루틴은 호출 스레드를 차단하지 않는다.
반응형