코틀린 코루틴 (8장 정리)

윤성현·2024년 12월 18일

코틀린 코루틴

목록 보기
8/11
post-thumbnail

8장. 잡과 자식 코루틴 기다리기

구조화된 동시성의 중요한 특성 중 세 가지는 Job 컨텍스트와 관련이 있다.
Job 과 연관된 코틀린 코루틴의 필수적인 작동 방식에 대해 알아보자!

서론

부모-자식 관계의 특성 👨‍👧‍👦

  • 자식은 부모로부터 컨텍스트를 상속받음
  • 부모는 모든 자식이 작업을 마칠 때까지 기다림 ⌛
  • 부모 코루틴이 취소되면 자식 코루틴도 취소됨 🔌
  • 자식 코루틴에서 에러가 발생하면, 부모 코루틴 또한 에러로 소멸함 💥
fun main(): Unit = runBlocking(CoroutineName("main")) {
	val name = coroutineContext[CoroutineName]?.name
	println(name) // main
	launch {
		delay(1000)
		val name = coroutineContext[CoroutineName]?.name
		println(name) // main
	}
}

Job이란 무엇인가? 🎫

  • 잡(job)은 수명을 가지고 있으며 취소 가능
  • Job은 인터페이스이지만 구체적인 사용법과 상태를 가지고 있다는 점에서 추상 클래스처럼 다룰 수도 있음
  • 잡의 수명은 상태로 나타냄
  • ‘Active’ 상태에서는 잡이 실행되고 코루틴은 잡을 수행함
  • 잡이 코루틴 빌더에 의해 생성되었을 때 코루틴의 본체가 실행되는 상태
  • 이 상태에서는 자식 코루틴을 시작할 수 있음
  • 대부분의 코루틴은 ‘Active’상태로 시작되며, 지연 시작되는 코루틴만 ‘New’ 상태에서 시작됨
  • ‘New’ 상태인 코루틴이 ‘Active’상태가 되려면 작업이 실행되어야 함
  • 코루틴이 본체를 실행하면 ‘Active’ 상태로 가게 됨
  • 실행이 완료되면 상태는 ‘Completing’으로 바뀌고 자식들을 기다림
  • 자식들의 실행도 모두 끝났다면 잡은 ‘Completed’로 바뀜
  • 만약 잡이 실행 도중에 취소되거나 실패하게 되면 ‘Cancelling’상태로 변경
  • 여기서 연결을 끊거나 자원을 반납하는 등의 후처리 작업을 할 수 있음
  • 후처리 작업이 완료되면 잡은 ‘Cancelled’상태가 됨
suspend fun main() = coroutineScope {
	// 빌더로 생성된 잡은
	val job = Job()
	println(job) // JobImpl{Completed}@ADD
	// 메서드로 완료시킬 때까지 Active 상태입니다.
	job.complete()
	println(job) // JobImpl{Completed}@ADD
	
	// launch는 기본적으로 활성화되어 있습니다.
	val activeJob = launch {
		delay(1000)
	}
	println(activeJob) // StandaloneCoroutine{Active}@ADD
	// 여기서 잡이 완료될 때까지 기다립니다.
	activeJob.join() // (1초 후)
	println(activeJob) // StandaloneCoroutine{Active}@ADD
	
	// launch는 New 상태로 지연 시작됩니다.
	val lazyJob = launch(start = CoroutineStart.LAZY) {
		delay(1000)
	}
	
	println(lazyJob) // LazyStandaloneCoroutine{New}@ADD
	
	lazyJob.start()
	println(lazyJob) // LazyStandaloneCoroutine{New}@ADD
	lazyJob.join() // (1초 후)
	println(lazyJob) // LazyStandaloneCoroutine{New}@ADD
}

Job 상태 다이어그램 🗺️

상태isActiveisCompletedisCancelled
New (지연 시작될 때 시작 상태)falsefalsefalse
Active (시작 상태 기본값)truefalsefalse
Completing (일시적인 상태)truefalsefalse
Cancelling (일시적인 상태)falsefalsetrue
Cancelled (최종 상태)falsetruetrue
Completed (최종 상태)falsetruefalse

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

  • 모든 코루틴 빌더는 자신만의 잡을 생성함
  • 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
	// 또는 coroutineContext[Job]!!
	println(job == parentJob) // false
	val parentChildren: Sequence<Job> = parentJob.children
	println(parentChildren.first() == job) // true
}
fun main(): Unit = runBlocking {
	launch(Job()) { // 새로운 잡이 부모로부터 상속받은 잡을 대체
		delay(1000)
		println("Will not be printed")
	}
}
// (아무것도 출력하지 않고, 즉시 종료됨)
  • 부모와 자식 사이에 아무런 관계가 없기 때문에 부모가 자식 코루틴을 기다리지 않음
  • 자식은 인자로 들어온 잡을 부모로 사용하기 때문에 runBlocking 과는 아무런 관련이 없게 됨
  • 코루틴이 자신만의 독자적인 잡을 가지고 있으면 부모와 아무런 관계가 없음
  • 부모-자식 관계가 없으면 구조화된 동시성을 잃게됨

자식들 기다리기 🕒

잡의 첫 번째 중요한 이점 : 코루틴이 완료될 때까지 기다리는 데 사용될 수 있음

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

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
// Job 인터페이스는 모든 자식을 참조할 수 있는 children 프로퍼티를 노출
// 모든 자식이 마지막 상태가 될 때까지 기다리는데 활용 가능
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

잡 팩토리 함수 🏭

JobJob() 팩토리 함수를 사용해서 코루틴 없이 Job을 만들 수 있음

팩토리 함수로 생성하는 잡은 어떤 코루틴과도 연관되지 않으며 컨텍스트로 사용될 수 있음

한 개 이상의 자식 코루틴을 가진 부모 잡으로 사용할 수 있음

‼️ Job() 팩토리 함수를 사용해 잡을 생성하고 다른 코루틴의 부모로 지정한 뒤에 join을 호출하면 모든 작업이 끝마쳐도 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")
}
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() }
}
  • Job()은 가짜 생성자 (실제 반환 타입은 Job이 아니라 하위 인터페이스인 CompletableJob)
public fun Job(parent: Job? = null): CompletableJob
  • complete(): Boolean
    • 잡을 완료하는 데 사용됨
    • complete 메서드를 사용하면 모든 자식 코루틴은 작업이 완료될 때까지 실행된 상태를 유지하지만, complete를 호출한 잡에서 새로운 코루틴이 시작될 수는 없음
    • 잡이 완료되면 실행 결과는 true가 되고, 그렇지 않을 경우 false가 됨
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
  • completeExceptionally(exception: Throwable): Boolean
    • 인자로 받은 예외로 잡을 완료시킴
    • 모든 자식 코루틴은 주어진 예외를 래핑한 cancellationException으로 즉시 취소
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")
}
// Rep0
// Rep1
// Done
  • complete 함수는 잡의 마지막 코루틴을 시작한 후 자주 사용됨
  • 이후에는 join 함수를 사용해 잡이 완료되는 걸 기다리기만 하면 됨
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
  • Job 함수의 인자로 부모 잡의 참조값을 전달 할 수 있음
  • 이때 부모 잡이 취소되면 해당 잡 또한 취소됨
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

0개의 댓글