구조화된 동시성
구조화된 동시성이란?
코루틴은 기본적으로 특정 코루틴 스코프 내에서 여러 코루틴이 협력하여 동작한다.
코루틴은 부모-자식 관계를 가지며, 이 부모 자식 관계가 구조화 된다. 이를 구조화된 동시성이라고 한다.
여기서 부모 코루틴이라 함은, 코루틴을 생성한 코루틴이라고 할 수 있다.
runBlocking 과 같은 코루틴은 스스로 코루틴 스코프를 만들어서 실행하므로, 본인이 루트 코루틴이 된다.
왜 코루틴은 이런 구조화된 동시성을 필요로 할까?
그것은 바로 코루틴을 효과적으로 제어하기 위함이다.
구조화된 동시성의 특징을 살펴보자.
- 부모의 실행환경을 상속받는다.
- 부모 코루틴의 취소는 자식에게 전파된다.
- 부모 코루틴은 자식 코루틴이 끝날때까지 기다린다.
아래에서 각 특징들에 대해 각각 살펴보도록 하자.
부모의 실행환경을 상속받는다.
부모의 실행환경은 무엇을 의미할까? 그리고 상속을 받는다는 것은 어떤 의미일까?
부모의 실행환경은 CoroutineContext 를 의미한다. 즉, 코루틴이 실행될 수 있는 환경을 의미한다.
그리고 상속을 받는다는 것은 부모 CoroutineContext의 Element 들을 가지는 CoroutineContext 를 자식 코루틴의 실행환경으로 사용한다는 의미이다.
아래 코드와 다이어그램을 살펴보자
fun main() = runBlocking { // coroutine #1
launch { // coroutine #1
// Job
}
}
위 runBlocking 으로 생성된 코루틴의 CoroutineContext 를 launch 가 상속을 받는다.
이 과정에서 자식 코루틴인 luanch 는 자신의 Job 을 병합한다.
즉, 부모 코루틴 컨텍스트를 상속하지만 Job 은 상속받지 않으며, 아래 그림처럼 Job 이 서로를 참조하게 된다.
이를 확인하기 위해서 coroutineContext[Job].parent 와 coroutineContext[Job].childrent 필드를 통해 확인할 수 있다.
fun main() = runBlocking {
val parentJob = coroutineContext[Job]
launch {
println(parentJob === coroutineContext[Job]?.parent)
println(parentJob?.children?.any { it == coroutineContext[Job] })
}
println()
}
>> true
>> true
부모 코루틴의 취소는 자식에게 전파된다.
코루틴은 취소될 수 있는데, 취소는 자식으로 전파된다. 이 경우 취소는 자식 방향으로만 전파가 된다.
부모 코루틴이 취소되었음에도, 자식 코루틴이 지속적으로 실행된다면 리소스가 낭비되기 때문에 취소가 전파된다.
간단한 예제를 살펴보자.
fun main() = runBlocking {
val job = launch {
println("launch1 start")
launch {
delay(1000)
println("nested launch1 end")
}
launch {
delay(1000)
println("nested launch2 end")
}
delay(500)
println("launch1 end")
}
delay(50)
job.cancel()
}
>> launch1 start
// 종료
부모 코루틴은 자식 코루틴이 끝날때까지 기다린다.
코루틴의 상태 중 Completing 상태가 있다. 코루틴의 상태와 관련된 내용은 아래 포스팅에서 다루고 있다.
https://pius712.tistory.com/43
completing 상태란, 부모 코루틴이 실행이 완료되었으나 자식 코루틴의 실행이 완료될때까지 가디리는 상황이다.
참고로 Job 의 상태 필드를 통해서 active 와 completing 을 구분할 수는 없다.
이 둘 모두 아래의 상태를 가진다.
- active : true
- canceld : false
- completed : false
CoroutineScope 와 CoroutineContext
참고로 코루틴에 대해 학습을 하다보면 코루틴과 CoroutineScope, CoroutineContext 사이의 관계에 대해 잘 이해가 안되는 순간들이 있다.
이것은 구조화된 동시성의 기준을 코루틴 자체라고 착각해서 발생한다.
예를들어 아래의 예시가 이런 혼란을 만드는 것 같다.
suspend fun main() = coroutineScope {
println(Thread.currentThread().name)
launch {
println(Thread.currentThread().name)
}
launch {
println(Thread.currentThread().name)
}
Unit
}
이 코드는 코루틴이 몇개일까? 구조화된 동시성은 어떻게 되는가? launch 의 부모는 누구인가?
아래에서 추가적으로 살펴보겠지만 이 경우 코루틴은 3개이다.
- main 이 코틀린에 의해 코루틴으로 시작 (1개)
- launch 코루틴 (2개)
구조화된 동시성의 계층 구조
CoroutineScope
├── 첫 번째 `launch` 코루틴 (부모: `coroutineScope`의 `Job`)
└── 두 번째 `launch` 코루틴 (부모: `coroutineScope`의 `Job`)
main 함수의 coroutine 은 EmptyCoroutineContext 에서 실행된다.
따라서, 코루틴은 맞지만 메인 함수내에서 다른 코루틴과 부모 관계를 이루지 않는다.
반면, coroutineContext 는 코루틴은 아니지만, Job 객체를 통해 launch 와 부모 자식 관계를 이룬다.
애시당초 main 코루틴은 Job이 없기 때문에 구조화되지 않는다.
중단함수와 마찬가지로 CoroutineScope 와 CoroutineContext 그 자체는 코루틴이 아니다.
CoroutineContext 에서 코루틴 빌더를 통해 코루틴을 시작해야 그것이 코루틴이 되는 것이다.
그리고 CoroutineContext 를 제공해주는 범위를 CoroutineScope 를 통해 제한하는 것이다.
CoroutineScope
코루틴 스코프는 코루틴이 사용되는 영역이자, 코루틴 컨텍스트가 유지되는 영역이다.
즉, 코루틴 스코프는 코루틴이 동작하기 위해 필요한 코루틴 컨텍스트가 제공하며, 코루틴 컨텍스트를 관리하는 역할을 한다.
launch, async 와 같은 코루틴 빌더는 CoroutineScope 의 확장함수로, 코루틴 빌더가 실행되기 위해서는 CoroutineScope 를 필요로한다.
예를들어, runBlocking 의 block 함수가 CoroutineScope 내에서 실행되기 때문이다.
구조화된 동시성은 무엇을 기준으로 관계를 이루는가?
위에서는 부모-자식 관계를 코루틴을 중심으로 설명하였다.
하지만 정확히 말하자면, 구조화된 동시성에서 부모-자식 관계는 Job 을 기준으로 정의된다.
이를 확인하기 위해 코루틴 스코프 내에서 coroutineContext[Job] 을 통해 디버깅해보기를 추천한다.
아래에서는 여러가지 예시를 통해 구조화된 동시성에 대해서 살펴보고자한다.
예시 1. coroutineScope
아래 코드를 보자. launch 의 부모는 runBlocking 일까?
suspend fun main() = runBlocking {
coroutineScope {
launch {
println("launch 1")
}
launch {
println("launch 2")
}
}
}
그렇지 않다. 이를 그림으로 살펴보자.
위 코드는 아래의 그림처럼 구조화된다. coroutineScope 는 scope 를 생성하며, CoroutineContext 에 자신의 Job 가진다.
예시2. Job
아래처럼 Job 객체를 생성한다음, launch 에 job 을 context 로 제공해보자.
그러면, launch 의 부모가 job 객체가 됨을 확인할 수 있다.
fun main = runBlocking {
val job = Job()
launch(CoroutineName("launch") + job) {
println(coroutineContext[Job]?.parent === job)
// >> true
}
}
예시3. 별도의 스코프
fun main() = runBlocking {
launch {
val job = coroutineContext[Job]
CoroutineScope(Dispatchers.IO).launch {
println(coroutineContext[Job]?.parent === job)
// >> false
}
}
}
위 코드는 아래와 같은 구조를 가지게 된다.
예시4. Job 이 없는 CoroutineScope
아래 코드의 경우, CustomScope 가 Job 을 가지고 있지 않다.
그렇기 때문에, launch 의 경우 부모 자식관계가 생기지 않으며 구조화된 동시성을 이룰 수 없게 된다.
fun main() = runBlocking {
val coroutineScope = CustomScope()
val job = coroutineScope.coroutineContext[Job]
coroutineScope.launch {
println(job)
println(coroutineContext[Job]?.parent)
}
Unit
}
class CustomScope : CoroutineScope {
override val coroutineContext: CoroutineContext
= newSingleThreadContext("CustomScope")
}
'자바와 코틀린' 카테고리의 다른 글
코루틴 - 코루틴 스레드 양보와 실행 스레드 (0) | 2024.09.17 |
---|---|
코루틴 - 중단함수 (0) | 2024.09.17 |
코루틴 - 코루틴 빌더와 Job (0) | 2024.09.17 |
코루틴 - CoroutineDispatcher (0) | 2024.09.17 |
프로세스와 스레드(운영체제 기본) (0) | 2024.07.29 |