훌륭한 개발자가 되기 위하여
구조화된 동시성 본문
구조화된 동시성
구조화된 동시성 원칙 : 비동기 작업을 구조화해 비동기 프로그래밍을 보다 안정적이고, 예측 가능하게 만드는 원칙
- 코루틴을 부모-자식 관계로 구조화해 코루틴이 보다 안전하게 관리되고 제어될 수 있다
- 코루틴 빌더 함수의 람다식 안에서 새로운 코루틴 빌더 함수를 호출하면 된다.
fun main() = runBlocking<Unit> {
launch {
println("부모 코루틴 실행")
launch {
println("자식 코루틴 실행")
}
}
}
실행 환경 상속
- 부모 코루틴은 자식 코루틴에게 실행 환경을 상속한다.
- 부모 코루틴이 자식 코루틴을 생성하면 부모 코루틴의 CoroutineContext가 자식 코루틴에게 전달된다.
- 자식 코루틴을 생성하는 코루틴 빌더 함수에 새로운 CoroutineContext 객체가 전달되면, 부모 코루틴에서 잔달받은 CoroutineContext 구성 요소들은 자식 코루틴 빌더 함수로 전달된 CoroutineContext 객체의 구성 요소들로 덮어 씌워진다.
상속되지 않는 Job
- launch나 async 함수는 호출 시마다 코루틴 추상체인 Job객체를 생성하는데 코루틴 제어에 Job객체가 필요하기 때문에 Job 객체를 부모 코루틴으로부터 상속받지 않는다.
- 즉, 코루틴 빌더를 통해 생성된 코루틴들은 서로 다른 Job을 가진다.
- 부모 코루틴으로부터 전달 받은 Job 객체는 코루틴을 구조화 하는데 사용된다.
- 코루틴 빌더가 호출되면 Job 객체는 새롭게 생성되지만, 생성된 Job 객체는 내부에 정의된 parent 프로퍼티를 통해 부모 코루틴의 Job 객체에 대한 참조를 가진다.
- 부모 코루틴의 Job 객체는 Sequence 타입의 children 프로퍼티를 통해서 자식 코루틴들의 Job에 대한 참조를 가진다.
Job의 프로퍼티 | 타입 | 설명 |
parent | Job? | 코루틴은 부모 코루틴이 없을 수 있고, 부모 코루틴이 있더라도 최대 하나이다. |
children | Sequence<Job> | 하나의 코루틴이 복수의 자식 코루틴을 가질 수 있다. |
구조화된 코루틴의 특성
1. 취소의 전파
- 코루틴으로 취소가 요청되면 자식 코루틴에 취소가 전파된다.
2. 부모 코루틴의 자식 코루틴에 대한 완료 의존성
- 부모 코루틴은 모든 자식 코루틴이 실행 완료되야 완료될 수 있다.
- 코루틴의 구조화는 큰 작업을 연관된 여러 작은 작업으로 나누는 방식으로 일어나는데, 작은 작업이 완료되야 큰 작업이 완료될 수 있기 때문이다.
CoroutineScope 사용해 코루틴 관리하기
- CoroutineScope는 내부에 코루틴 실행 환경인 CoroutineContext를 가진 단순한 인터페이스
- CoroutineScope 생성 함수를 통해 CoroutineScope 객체를 생성할 수 있다
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
fun main() {
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행 완료")
}
Thread.sleep(1000L)
}
// 실행 결과
[DefaultDispatcher-worker-1 @coroutine#1] 코루틴 실행 완료
CoroutineScope 객체가 코루틴에게 실행 환경을 제공하는 방식
👉 launch, async 함수는 다음 과정을 통해 CoroutineScope 객체로부터 실행 환경을 제공 받아 실행 환경을 설정한다.
- 수신 객체인 CoroutineScope으로부터 CoroutineContext 객체를 제공 받는다.
- 제공 받은 CoroutineContext 객체에 launch 함수의 context 인자로 넘어온 CoroutineContext를 더한다.
- 생성된 CoroutineContext에 새로운 Job 객체를 생성해 더한다.
👉 이 때 CoroutineContext를 통해 전달되는 Job객체는 새로 생성된 Job 객체의 부모 Job이 된다.
CoroutineScope에 속한 코루틴의 범위
- CoroutineScope 객체는 특정 범위의 코루틴을 제어하는 역할을 한다.
- 범위는 CoroutineScope의 Job에 해당하는 하위의 모든 코루틴이다.
- 범위를 따로 지정하고 싶으면 임의의 CoroutineScope를 생성하면 된다.
CoroutineScope 취소하기
- CoroutineScope 인터페이스는 확장함수로 cancel 함수를 가지고 cancel 함수는 CoroutineScope 범위에 속한 모든 코루틴을 취소한다.
- 이 때, 취소를 위해 CoroutineScope의 coroutineContext의 Job을 취소하는 방법을 사용한다. ✔ 취소의 전파 특성 사용
코루틴의 구조화와 Job
- runBlocking 함수를 호출하면 부모 Job이 없는 루트 Job 객체가 생성된다.
- 루트 Job : 부모 Job 객체가 없는 구조화의 시작점 역할을 하는 Job 객체
- 루트 코루틴 : 이 Job 객체에 의해 제어되는 코루틴
Job 구조화 깨기
1. CoroutineScope 사용
- CoroutineScope 생성 함수는 새로운 루트 Job을 가진 CoroutineContext를 생성한다.
2. Job 객체 생성해 구조화 깨기
- Job 생성 함수로 새로운 루트 Job을 생성할 수 있다.
fun main() = runBlocking<Unit> {
val newRootJob = 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}] 코루틴 실행")
}
}
}
Job 생성 함수
- Job 생성 함수는 부모 Job을 인자로 받을 수 있다.
- parent 인자가 입력되지 않으면 parent가 null이 되어 루트 Job이 생성된다.
- parent 인자가 입력되면 해당 job을 부모로 하는 Job이 생성된다.
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
- Job 생성 함수를 이용해 생성된 Job객체는 자동으로 실행 완료되지 않는다.
- Job 객체를 종료하기 위해서는 complete() 함수를 호출해서 명시적으로 완료 호출을 해줘야 한다.
runBlocking 함수
- runBlocking 함수가 호출되면, 호출부의 스레드를 차단하고 배타적으로 사용하는 코루틴이 만들어진다.
- 호출부의 스레드를 사용할 수 있는 코루틴은 자신과 자식 코루틴들뿐이다.
- 호출부의 스레드는 runBlocking 함수가 생성한 코루틴이 실행 완료될 때까지 다른 작업에 사용될 수 없다.
'안드로이드' 카테고리의 다른 글
일시 중단 함수와 코루틴 (0) | 2025.01.13 |
---|---|
코루틴 예외 처리 (0) | 2025.01.12 |
CoroutineContext란 (0) | 2025.01.10 |
코루틴으로부터 결과 수신 받기 (0) | 2025.01.08 |
코루틴 제어 (0) | 2025.01.07 |