관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석- 구조화된 동시성(Structured Concurrency) 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석- 구조화된 동시성(Structured Concurrency)

기록하는 백앤드개발자 2024. 6. 3. 10:10
반응형

ㅁ 들어가며

 코틀린 코루틴의 정석 책을 보고 정리한 글입니다.

 

ㅁ 비구조적 동시성의 한계

ㅇ 책에서 설명하는 구조화된 동시성을 이해하기 위해서는 비구조적 동시성의 한계를 우선 알아야 한다. 

ㅇ 작업의 단위가 단편적인 경우도 있지만, 상호의존적인 여러 개의 하위 작업들로 나누어지는 경우가 있다. 예전에는 이를 단일 스래드로 순차적으로 처리하면서 에러 발생 시 Exception처리를 하고 종료를 하면 큰 문제가 발생하지 않았다. 

ㅇ 하지만 고가용적인 방식으로 각각의 작업이 비동기로 서로 독립적으로 수행하면 전체적인 작업은 빠르게 병렬처리되지만 제어권이 개별적으로 있어 에러헨들링은 어렵게 되었다.

 

ㅁ 구조적 동시성의 개념과 필요성

ㅇ 구조적 동시성(Structured Concurrency)이란 부모 작업과 하위 작업간의 트리구조를 이루어 작업 간에 유지 보수성을 높이고 안정적인 동시성 코드를 작성할 수 있도록 하는 동시성 프로그래밍 접근 방식이다.

ㅇ 구조적 동시성은 코드의 구문 구조(syntactic structure)를 사용해 서로 다른 스레드에서 실행 중인 연관된 작업들을 하나의 작업 단위 (single unit of work)로 방식으로, 이를 통해 오류 처리나 취소를 간소화하고 안정성과 관측성을 높이고자 한다.

ㅇ 구조적 동시성이란 용어는 용어는 Martin Sústrik에 의해 만들어졌고, Nathaniel J. Smith에 의해 대중화되었다.

 

ㅇ 코루틴은 이러한 구조적 동시성을 구현하였다.

ㅇ 구조화된 동시정의 원칙이란 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적으로 수행할 수 있게 한다.

ㅇ 코루틴은 부모-자식 관계로 구조화된 코루틴으로 보다 안전하게 비동기 작업을 관리제어 할 수 있다.

 

참조: 망나니 개발자 - [Java] 기존 동시성 프로그래밍의 한계와 새롭게 도입될 구조적 동시성(Structured Concurrency)

7장 구조화된 동시성(Structured Concurrency)

- 코루틴 실행 환경 상속 : 부모 코루틴의 실행 환경이 자식 코루틴에 상속된다.
- 구조화를 통한 작업 제어 : 부모 코루틴은 자식 코루틴이 완료를 보장하고, 부모가 취소되면 자식도 취소된다. 
- CoroutineScope를 사용한 코루틴 관리 : CoroutineScope를 사용해 코루틴의 실행범위를 제한할 수 있다.
- 코루틴의 구조체에서 Job의 역할

ㅁ 코루틴의 구조화된 동시성의 기본 원칙

한 작업이 동시 진행 중인 하위 작업들로 분할되면, 그들 모두 같은 위치(작업의 코드 블록)로 돌아간다.

ㅇ 비유적으로, 스타크레프트의 케리어처럼 상위 작업을 대신하여 인터셉터가 하위 작업을 수행한다. 

   ㄴ 케리어에게 후퇴명령을 내리면 전체 인터셉터는 현재 작업을 취소하고 케리어로 복귀하듯, 구조화된 작업은 하나의 작업처럼 관리 할 수 있다. 

 

ㅇ 코루틴의 구조화된 트리구조로 parent가 종료되면, parent의 모든 children이 취소된다.

ㅇ child에서 exception이 발생하여 종료되면, exception은 parent로 전파되어 parent를 취소한다. 

 child가 명시적으로 취소되면 parent로 취소가 전파되지 않는다.

 

ㅁ 코루틴 실행환경 상속

fun main() = runBlocking<Unit> {
  val coroutineContext = newSingleThreadContext("FamilyThread") + CoroutineName("코루틴상속")
  launch(coroutineContext){ // 부모 코루틴 생성
    println("[${Thread.currentThread().name}] 나는 부모다")
    launch {  // 자식 코루틴 생성
      println("[${Thread.currentThread().name}] 나는 자식이다")
    }
  }
}
/*
[FamilyThread @코루틴상속#2] 나는 부모다
[FamilyThread @코루틴상속#3] 나는 자식이다
 */

ㅇ 동시성 및 하위 작업의 개별성을 유지하기 위해 일부 정보는 상속되지만 하위작업의 고유 Job은 생성된다.

ㅇ 그래서 부모 코루틴은 자식 코루틴에게 실행 환경을 일부 상속한다.

 

fun main() = runBlocking<Unit> {
  val coroutineContext = newSingleThreadContext("FamilyThread") + CoroutineName("코루틴상속")
  launch(coroutineContext){ // 부모 코루틴
    println("[${Thread.currentThread().name}] 나는 부모다")
    // 부모로부터 독립하겠다.
    launch(CoroutineName("자식독립")) {  // 자식 코루틴
      println("[${Thread.currentThread().name}] 나는 독립한 자식이다")
    }
  }
}
/*
[FamilyThread @코루틴상속#2] 나는 부모다
[FamilyThread @자식독립#3] 나는 독립한 자식이다
 */

ㅇ 코루틴 빌더 함수에 전달된 CoroutineContext를 통해 부모 코루틴의 실행 환경 중 일부 또는 전부를 덮어쓸 수 있다.

ㅇ 주의점으로 Job객체는 상속되지 않고 코루틴 빌더 함수가 호출되면 새롭게 생성된다는 점이다.

 

fun main() = runBlocking<Unit> {
  val runBlockingJob = coroutineContext[Job] // 부모 코루틴의 CoroutineContext로부터 부모 코루틴의 Job 추출
  println("[${Thread.currentThread().name}] 나는 부모다")
  // 부모로부터 독립하겠다.
  launch(CoroutineName("자식독립")) {  // 자식 코루틴
    val launchJob = coroutineContext[Job] // 자식 코루틴의 CoroutineContext로부터 자식 코루틴의 Job 추출

    println("[${Thread.currentThread().name}] 나는 독립한 자식이다")

    if (runBlockingJob === launchJob) {
      println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 동일합니다")
    } else {
      println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다")
    }
  }
}
/*
[main @coroutine#1] 나는 부모다
[main @자식독립#2] 나는 독립한 자식이다
runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다
 */

ㅇ launch나 async를 포함한 모든 코루틴 빌더 함수는 호출 때마다 코루틴 추상체인 Job을 새롭게 생성한다.

 그래서 launch 코루틴이 runBlocking코루틴으로부터 실행환경을 상속 받았음에도 서로 다른 Job객체를 가진다.

 

구조화에 사용되는 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
*/

ㅇ 부모 코루틴과 자식 코루틴은 parentJob이 childJob과 동일하지 않다.

ㅇ 자식 코루틴의 Job이 갖고 있는 Parent는 부모 코루틴의 Job이다.

  ㄴ launch 코루틴은 runBlocking 코루틴의 자식 코루틴이기 때문에 childJob의 parent 프로퍼티는 parentJob을 가리킨다.

ㅇ 부모 코루틴의 Job은 자식 코루틴의 Job에 대해 참조한다.

  ㄴ runBlocking은 자식 코루틴으로 launch 코루틴을 갖기 때문에 parentJob은 children 프로퍼티를 통해 childJob에 대한 참조를 가진다.

ㅇ parent 프로퍼티가 null인 Job은 구조화의 시작점 역할을 하는 루트 Job 객체이다.

ㅇ Job은 자식 Job들을 Sequence<Job>타입의 children 프로퍼티를 통해 참조한다.

ㅇ Job은 코루틴의 구조화에 핵심적인 역할을 한다. 

소결 : 부모코루틴과 자식 코루틴은 서로 다른 Job객체를 가지며, 코루틴 빌더가 호출될 때 Job객체가 새롭게 생성된다. 다만, 부모 코루틴의 Job객체에 대해 참조하며, 부모 코루틴의 Job 객체 또한 children 프로퍼티를 통해 자식 코루틴의 Job객체에 대한 참조를 갖는 것을 확인할 수 있다.

 

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

ㅇ 코루틴의 구조화는 하나의 큰 비동기 작업을 작은 비동기 작업으로 나눌 때 일어난다. 

ㅇ 서버로부터 데이터를 다운받는 상위 작업이 실제로는 여러 서버에 데이터를 다운받는 하위 작업들로 분할하여 비동기로 병렬처리된다. 

 

취소전파

ㅇ 상위 작업에서 취소가 발생하면 하위 작업들로 전파가 된다. 

ㅇ 중간 단계에서 취소가 발생하면 중간 하위 작업들에게는 취소가 전파되지만, 상위로는 취소가 전파되지 않는다.

ㅇ 자식으로만 취소가 전파되는 이유는 자식이 부모작업의 일부분이기 때문이다. 

 

fun main() = runBlocking<Unit> {
  val carrierJob = launch(Dispatchers.IO) { // 부모 코루틴 생성
    val interceptorDeferred: List<Deferred<String>> = listOf("ic1", "ic2", "ic3", "ic4", "ic5").map {
      async { // 자식 코루틴 생성
        delay(1000L) // 공격시간
        println("${it}가 공격하였습니다")
        return@async "[${it}] 공격"
      }
    }
    val results: List<String> = interceptorDeferred.awaitAll() // 모든 코루틴이 완료될 때까지 대기

    println(results) // 결과표시
  }
}
/*
ic1가 공격하였습니다
ic3가 공격하였습니다
ic5가 공격하였습니다
ic4가 공격하였습니다
ic2가 공격하였습니다
[[ic1] 공격, [ic2] 공격, [ic3] 공격, [ic4] 공격, [ic5] 공격]
*/

ㅇ 취소전파를 위한 케리어 + 인터셉터 구성

 

~~~~
    println(results) // 결과표시
  }
  carrierJob.cancel() // 상위작업 취소
}
/* 결과가 없음
*/

ㅇ 상위작업을 취소하면 아무 것도 수행하지 않는다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val carrierJob = launch(Dispatchers.IO) { // 부모 코루틴 생성
    println("케리어 공격 시작, [${getElapsedTime(startTime)}]")
    val interceptorDeferred: List<Deferred<String>> = listOf("ic1", "ic2", "ic3", "ic4", "ic5").map {
      async { // 자식 코루틴 생성
        delay(1000L) // 공격시간
        println("${it}가 병렬공격, [${getElapsedTime(startTime)}]")
        return@async "[${it}] 공격"
      }
    }
  }
  carrierJob.invokeOnCompletion {
    println("모든 공격 완료, [${getElapsedTime(startTime)}]")
  }
}
fun getElapsedTime(startTime: Long): String = "시간체크: ${System.currentTimeMillis() - startTime}ms"
/*
케리어 공격 시작, [시간체크: 5ms]
ic4가 병렬공격, [시간체크: 1023ms]
ic1가 병렬공격, [시간체크: 1023ms]
ic3가 병렬공격, [시간체크: 1023ms]
ic5가 병렬공격, [시간체크: 1023ms]
ic2가 병렬공격, [시간체크: 1023ms]
모든 공격 완료, [시간체크: 1043ms]
*/

ㅇ 부모 코루틴은 모든 자식 코루틴의 실행을 보장하여 여러 분산된 작업이 완료되어야 부모작업이 완료된다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val carrierJob = launch(Dispatchers.IO) { // 부모 코루틴 생성
    println("케리어 공격 시작, [${getElapsedTime(startTime)}]")
    listOf("ic1", "ic2", "ic3", "ic4", "ic5").map {
      async { // 자식 코루틴 생성
        println("${it}가 병렬공격, [${getElapsedTime(startTime)}]")
        while (true) delay(1000)
        return@async "[${it}] 공격"
      }
    }
  }
  carrierJob.invokeOnCompletion {
    println("부모 코루틴을 취소하여도 콜백은 실행, [${getElapsedTime(startTime)}]")
  }
  carrierJob.cancel() // 부모 코루틴 취소
}
/*
케리어 공격 시작, [시간체크: 3ms]
부모 코루틴을 취소하여도 콜백은 실행, [시간체크: 34ms]
*/

ㅇ 부모 코루틴을 취소하여도 콜백은 실행된다.

 

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밀리초간 대기
  println(  "isActive >> ${parentJob.isActive}")
}
/*
[지난 시간: 6ms] 부모 코루틴이 실행하는 마지막 코드
isActive >> true
[지난 시간: 1017ms] 자식 코루틴 실행 완료
[지난 시간: 1019ms] 부모 코루틴 실행 완료
*/

 

ㅇ 부모 코루틴은 자식 코루틴이 완료될 때까지 완료하지 않는다. 자식이 실행 중이면 부모도 "실행 완료 중" 상태를 가진다.

ㅇ 부모 코루틴이 취소되면 모든 자식 코루틴에게 전파되어 자식도 취소되지만, 자식이 취소되어도 부모가 취소되지는 않는다.

 

ㅁ CoroutineScope를 사용해 코루틴 관리하기

ㅇ CoroutineScope를 살펴보면 CoroutineContext를 가진다.

ㅇ CoroutineContext는 Element 정보들를 가지고 있다.

 

CoroutineScope의 구조

 

@OptIn(ExperimentalStdlibApi::class)
fun main() {
  val 스코프 = CoroutineScope(CoroutineName("코루틴") + Dispatchers.IO)
  스코프.launch(CoroutineName("코루틴런처")) {
    println(this.coroutineContext[CoroutineName])
    println(this.coroutineContext[CoroutineDispatcher])
    val 런처잡 = this.coroutineContext[Job]
    val 스코프잡 = 스코프.coroutineContext[Job]
    println("런처잡?.parent === 스코프잡 >> ${런처잡?.parent === 스코프잡}")
  }
  Thread.sleep(1000L)
}
/*
CoroutineName(코루틴런처)
Dispatchers.IO
런처잡?.parent === 스코프잡 >> true
*/

ㅇ CoroutineScope를 사용해 코루틴의 실행 범위를 제한한다.

ㅇ CoroutineScope 인터페이스는 코루틴 실행 환경인 CoroutineContext를 가진 인터페이스로 확장 함수로 launch, async 등의 함수를 가진다.

ㅇ launch나 async가 호출되면 CoroutineScope로부터 실행환경을 제공받아 코루틴이 실행된다.

 

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    println("[${Thread.currentThread().name}] Coroutine1 실행")
    launch(CoroutineName("Coroutine3")) {
      println("[${Thread.currentThread().name}] Coroutine3 실행")
    }
    // 기존 CoroutineScope를 빠져나가 새로운 계층 구조로 만들어진 Coroutine4에서 실행된다.  
    CoroutineScope(Dispatchers.IO).launch(CoroutineName("Coroutine4")) {
      println("[${Thread.currentThread().name}] Coroutine4 실행")
    }
  }

  launch(CoroutineName("Coroutine2")) {
    println("[${Thread.currentThread().name}] Coroutine2 실행")
  }
}
/*
[main @Coroutine1#2] Coroutine1 실행
[main @Coroutine2#3] Coroutine2 실행
[DefaultDispatcher-worker-1 @Coroutine4#5] Coroutine4 실행
[main @Coroutine3#4] Coroutine3 실행
*/

ㅇ 기존 CoroutineScope를 빠져나가 새로운 계층 구조로 만들어진 Coroutine4에서 실행된다.

 

CoroutineScope 취소하기

fun main() = runBlocking<Unit> {
  launch(CoroutineName("Coroutine1")) {
    println("[${Thread.currentThread().name}] Coroutine1 실행")
    launch(CoroutineName("Coroutine3")) {
      println("[${Thread.currentThread().name}] Coroutine3 실행")
    }
    // 기존 CoroutineScope를 빠져나가 새로운 계층 구조로 만들어진 Coroutine4에서 실행된다.
    CoroutineScope(Dispatchers.IO).launch(CoroutineName("Coroutine4")) {
      println("[${Thread.currentThread().name}] Coroutine4 실행")
    }
    // 취소방법
    this.cancel()
  }

  launch(CoroutineName("Coroutine2")) {
    println("[${Thread.currentThread().name}] Coroutine2 실행")
  }
}
/*
[main @Coroutine1#2] Coroutine1 실행
[DefaultDispatcher-worker-1 @Coroutine4#5] Coroutine4 실행
[main @Coroutine2#3] Coroutine2 실행
*/

ㅇ CoroutineScope에 대해 cancel를 호출하는 것은 CoroutineScope가 가진 CoroutineContext의 Job에 대해 cancel를 호출한다.

ㅇ Coroutine4는 새로운 계층 구조로 넘어갔기 때문에 중지를 해도 정상작동된다.

ㅇ 별도의 범위를 갖는 CoroutineScope를 생성해 코루틴의 구조화를 깰 수 있다.

 

CoroutineScope 활성화 확인

fun main() = runBlocking<Unit> {
  val whileJob: Job = launch(Dispatchers.IO) {
    // while문이 반복될 때마다 취소요청여부를 확인한다.
    while (this.isActive)
      println("작업 실행")
  }
  delay(100)
  whileJob.cancel()
}

ㅇ CoroutineScope의 활성화 상태를 isActive 프로퍼티를 통해 확인할 수 있다.

ㅇ CoroutineScope의 isActive 프로퍼티는 CoroutineScope가 가진 CoroutineContext의 Job에 대한 isActive 프로퍼티를 확인하는 것이다.

 

ㅁ 구조화와 Job

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  println("${getElapsedTime(Thread.currentThread().name, startTime)}] main 코루틴 root Job이 생성")

  launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
    delay(1000L)
    println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine1 실행")
    launch(CoroutineName("Coroutine1-1")) { // Coroutine3 실행
      delay(1000L)
      println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine1-1 실행")
    }
    launch(CoroutineName("Coroutine1-2")) { // Coroutine4 실행
      delay(1000L)
      println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine1-2 실행")
    }
  }
  launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
    delay(1000L)
    println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine2 실행")
    launch(CoroutineName("Coroutine2-1")) { // Coroutine5 실행
      delay(1000L)
      println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine2-1 실행")
    }
    launch(CoroutineName("Coroutine2-2")) { // Coroutine5 실행
      delay(1000L)
      println("${getElapsedTime(Thread.currentThread().name, startTime)}] Coroutine2-2 실행")
    }
  }
  delay(1000L)
  println("${getElapsedTime(Thread.currentThread().name, startTime)} main 코루틴 root Job 실행완료")
}

fun getElapsedTime(name: String, startTime: Long): String =
  "[${name}][지난 시간: ${System.currentTimeMillis() - startTime}ms]"
  
/*
[main @coroutine#1][지난 시간: 0ms]] main 코루틴 root Job이 생성
[main @coroutine#1][지난 시간: 1020ms] main 코루틴 root Job 실행완료
[main @Coroutine1#2][지난 시간: 1025ms]] Coroutine1 실행
[main @Coroutine2#3][지난 시간: 1027ms]] Coroutine2 실행
[main @Coroutine1-1#4][지난 시간: 2029ms]] Coroutine1-1 실행
[main @Coroutine1-2#5][지난 시간: 2031ms]] Coroutine1-2 실행
[main @Coroutine2-1#6][지난 시간: 2032ms]] Coroutine2-1 실행
[main @Coroutine2-2#7][지난 시간: 2032ms]] Coroutine2-2 실행
*/

ㅇ runBlocking을 통해 루트 코루틴이 생성되고 launch가 새로운 Coroutine1,2를 실행한다.

ㅇ Coroutine1,2는 다시 하위 코루틴을 실행하여 구조화된 Job을 실행할 수 있다.

ㅇ 하위의 코루틴은 병렬로 처리되는 특징을 확인할 수 있다.

ㅇ 스타 케리어가 공격을 시작하면 하위 인터셉터가 동시에 작동되는 구조이다. ^^

 

fun main() = runBlocking<Unit> { // 루트 Job 생성
  val newScope = CoroutineScope(Dispatchers.IO) // 새로운 루트 Job 생성
  newScope.launch(CoroutineName("newRoot")) {
    launch(CoroutineName("newCoroutine")) {
      delay(100L)
      println("runBlocking은 이 코루틴의 실행을 기다려 주지 않는다. 그래서 로그도 남지 않음")
    }
  }
}
/* 종료 프로세스만 나올 뿐 다른 로그는 없다.
종료 코드 0(으)로 완료된 프로세스
*/

ㅇ Job함수를 호출해 Job을 생성할 수 있으며, 이를 사용해 코루틴의 구조화를 다중화 할 수 있다.

ㅇ newScope가 새로운 루트가 되면서 runBlocking 코루틴은 newScope의 코루틴을 기다리지 않기 때문에 아무 로그도 확인 할 수 없다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  // 새로운 root Job을 선언하여 다중처리가 가능하다.
  val newRootJob = Job() // parent가 null인 루트 Job 생성

  // 작업1 병렬처리
  launch(CoroutineName("작업1") + newRootJob) {
    launch(CoroutineName("작업1-1")) {
      delay(100L)
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  작업1-1 종료")
    }
    launch(CoroutineName("작업1-2")) {
      delay(100L)
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  작업1-2 종료")
    }
  }
  // 작업2 병렬처리
  launch(CoroutineName("작업2") + newRootJob) {
    launch(CoroutineName("작업2-1")) {
      delay(100L)
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  작업2-1 종료")
    }
  }

  delay(1000L) // 현실에서는 쓰지 않지만, 출력을 확인하기 위해 사용
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  main 종료")
}
/*
[main @작업1-1#4][지난 시간: 111ms]  작업1-1 종료
[main @작업1-2#5][지난 시간: 121ms]  작업1-2 종료
[main @작업2-1#6][지난 시간: 121ms]  작업2-1 종료
[main @coroutine#1][지난 시간: 1008ms]  main 종료
*/

ㅇ 루트Job은 parent 없이 Job()으로 생성할 수 있다. 

ㅇ delay는 실재로 사용하지 않지만 결과 출력을 위해 사용한다. 

ㅇ 7.4.2.2 구조화 깨기라고 표현했지만, 다중화로 표현할 수 있다고 생각한다.

  ㄴ 결과를 추적할 필요 없는 병렬처리 가능한 작업은 job을 생성하여 다른 스레드로 처리할 수 있다.

ㅇ Job 생성 함수를 통해 생성된 Job는 자동으로 실행 완료되지 않으므로 Job에 대해 complete를 호출해 명시적으로 완료 처리해야 한다.

 

~~~~~
  newRootJob.cancel() // 루트 Job 취소
  delay(1000L) // 현실에서는 쓰지 않지만, 출력을 확인하기 위해 사용
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  main 종료")
}
/*
[main @coroutine#1][지난 시간: 1009ms]  main 종료
*/

ㅇ newRootJob을 cancel하면 하위의 job이 취소된다.

 

~~~
// 작업2 병렬처리
  launch(CoroutineName("작업2") + newRootJob) {
    launch(CoroutineName("작업2-1") + Job()) {
      delay(100L)
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  작업2-1 종료")
    }
  }

  delay(50L) // 모든 코루틴이 생성될 때까지 대기
  newRootJob.cancel()
  delay(1000L) // 현실에서는 쓰지 않지만, 출력을 확인하기 위해 사용
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}]  main 종료")
}
/*
[main @작업2-1#6][지난 시간: 112ms]  작업2-1 종료
[main @coroutine#1][지난 시간: 1067ms]  main 종료
*/

ㅇ 여기에서 작업2-1만 예외로 두고 싶다면 Job()을 이용해 분기시킬 수 있다.

ㅇ delay(50)는 코루틴이 생성되고 cancel하기 위해 주어졌다.

ㅇ newRootJob가 rootJob인데 작업2-1도 신규 rootJob이 할당되면서 제어 분기를 탔다.

 

fun main() = runBlocking<Unit> {
  launch(CoroutineName("부모작업")) {
    val 부모작업Job = this.coroutineContext[Job] // Coroutine1의 Job
    val 하위작업Job = Job(parent = 부모작업Job)
    launch(CoroutineName("하위작업") + 하위작업Job) {
      delay(100L)
      println("[${Thread.currentThread().name}] 하위작업 실행")
    }
    하위작업Job.complete() // 명시적으로 하위작업을 종료되어야 부모작업이 실행 완료 상태가 된다. 
  }
}
/*
[main @Coroutine2#3] 코루틴 실행
종료 코드 0(으)로 완료된 프로세스
*/

 

ㅇ 부모Job을 지정하기

ㅇ 하위작업은 부모작업을 상속받을 수 있다.

ㅇ 명시적으로 하위작업을 종료되어야 부모작업이 실행 완료 상태가 된다.

  ㄴ 그렇지 않으면 계속 하위작업은 실행상태가 되고 부모작업은 '실행완료 중 상태'가 된다.

 

ㅁ runBlocking과 launch의 차이

ㅇ runBlocking은 호출되면 새로운 코루틴의 스레드가 완료될 때까지 호출부의 수레드를 차단한다.

 

fun main() = runBlocking<Unit> { // runBlocking 코루틴
  val startTime = System.currentTimeMillis()
  runBlocking { // 하위 runBlocking 코루틴
    delay(1000L)
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 하위 코루틴 종료")
  }
  launch {
    delay(1500L)
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] launch 코루틴 종료")
  }
}
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

/*
[main @coroutine#2][지난 시간: 1009ms] 하위 코루틴 종료
[main @coroutine#3][지난 시간: 2535ms] launch 코루틴 종료
*/

ㅇ runBlocking 코루틴 빌더는 생성된 코루틴이 완료될 때까지 호출 스레드를 차단한다.

fun main() = runBlocking<Unit> { // runBlocking 코루틴
  val startTime = System.currentTimeMillis()
  launch {
    delay(1500L)
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] launch 코루틴 종료")
  }
  runBlocking { // 하위 runBlocking 코루틴
    delay(1000L)
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] runBlocking 코루틴 종료")
  }
}
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"
/*
[main @coroutine#3][지난 시간: 1012ms] runBlocking 코루틴 종료
[main @coroutine#2][지난 시간: 1510ms] launch 코루틴 종료
*/

ㅇ 반면에 launch 코루틴 빌더로 생성된 코루틴은 호출 스레드를 차단하지 않는다.

 

ㅁ 함께 보면 좋은 사이트

 조세영 - 코틀린 코루틴의 정석

반응형
Comments