CoroutineContext는 원소나 원소들의 집합을 나타내는 인터페이스이다.
Job, CoroutineName, CoroutineDispatcher와 같은 Element 객체들이 인덱싱된 집합이라는 점에서 Map이나 Set과 같은 컬렉션이랑 비슷한 개념이다.
특이한 점은 각 Element 또한 CoroutineContext라는 점이다.
그리고 컨텍스트에서 모든 원소는 식별할 수 있는 유일한 Key를 가지고 있다. 각 키는 주소로 비교가 된다.
fun main() {
val name: CoroutineName = CoroutineName("A name")
val element: CoroutineContext.Element = name
val context: CoroutineContext = element
val job: Job = Job()
val jobElement: CoroutineContext.Element = job
val jobContext: CoroutineContext = jobElement
}
CoroutineName이나 Job은 CoroutineContext 인터페이스를 구현한 CoroutineContext.Element를 구현한다.
data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
override fun toString(): String = "CoroutineName($name)"
companion object Key : CoroutineContext.Key<CoroutineName>
}
interface Job : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<Job>
// ...
}
CoroutineName이나 Job은 타입이나 클래스가 아닌 컴패니언 객체이다. 따라서, 찾거나 사용시 바로 사용하면 된다.
fun main() {
val ctx: CoroutineContext = CoroutineName("A name")
val coroutineName: CoroutineName? = ctx[CoroutineName]
// 또는 ctx.get(CoroutineName)
println(coroutineName?.name) // A name
val job: Job? = ctx[Job] // 또는 ctx.get(Job)
println(job) // null
}
CoroutineContext는 컬렉션 처럼 get 또는 대괄호를 사용해서 원소를 찾을 수 있다.
원소가 컨텍스트에 있으면 반환되고 없으면 null이 반환된다는 점에서 Map과 비슷하다.
fun main() {
val ctx1: CoroutineContext = CoroutineName("Name1")
println(ctx1[CoroutineName]?.name) // Name1
println(ctx1[Job]?.isActive) // null
val ctx2: CoroutineContext = Job()
println(ctx2[CoroutineName]?.name) // null
println(ctx2[Job]?.isActive) // 'Active' 상태이므로 true이다.
// 빌더를 통해 생성되는 잡의 기본 상태가 'Active' 상태이므로 true가 된다.
val ctx3 = ctx1 + ctx2
println(ctx3[CoroutineName]?.name) // Name1
println(ctx3[Job]?.isActive) // true
}
CoroutineContext의 정말 유용한 기능은 두 개의 CoroutineContext를 합쳐 하나의 CoroutineContext로 만드는 것이다.
다른 키를 가진 두 원소를 더해 만들어진 컨텍스트는 두 가지 키를 모두 가진다.
fun main() {
val ctx1: CoroutineContext = CoroutineName("Name1")
println(ctx1[CoroutineName]?.name) // Name1
val ctx2: CoroutineContext = CoroutineName("Name2")
println(ctx2[CoroutineName]?.name) // Name2
val ctx3 = ctx1 + ctx2
println(ctx3[CoroutineName]?.name) // Name2
}
CoroutineContext에 같은 키를 가진 또 다른 원소가 더해지면 Map처럼 새로운 원소가 기존 원소를 대체한다.
fun main() {
val empty: CoroutineContext = EmptyCoroutineContext
println(empty[CoroutineName]) // null
println(empty[Job]) // null
val ctxName = empty + CoroutineName("Name1") + empty
println(ctxName[CoroutineName]) // CoroutineName(Name1)
}
CoroutineContext는 컬렉션이므로 빈 컨텍스트 또한 만들 수 있다.
빈 컨텍스트는 원소가 없으므로, 다른 컨텍스트에 더해도 아무런 변화가 없다.
fun main() {
val ctx = CoroutineName("Name1") + Job()
println(ctx[CoroutineName]?.name) // Name1
println(ctx[Job]?.isActive) // true
val ctx2 = ctx.minusKey(CoroutineName)
println(ctx2[CoroutineName]?.name) // null
println(ctx2[Job]?.isActive) // true
val ctx3 = (ctx + CoroutineName("Name2"))
.minusKey(CoroutineName)
println(ctx3[CoroutineName]?.name) // null
println(ctx3[Job]?.isActive) // true
}
minusKey 함수에 키를 넣는 방식으로 원소를 컨텍스트에서 제거할 수 있다.
fun CoroutineScope.log(msg: String) {
val name = coroutineContext[CoroutineName]?.name
println("[$name] $msg")
}
fun main() = runBlocking(CoroutineName("main")) {
log("Started") // [main] Started
val v1 = async {
delay(500)
log("Running async") // [main] Running async
42
}
launch {
delay(1000)
log("Running launch") // [main] Running launch
}
log("The answer is ${v1.await()}") // [main] The answer is 42
}
CoroutineContext는 코루틴의 데이터를 저장하고 전달하는 방법이다.
부모-자식 관계의 영향 중 하나로 부모는 기본적으로 컨텍스트를 자식에게 전달한다.
fun main() = runBlocking(CoroutineName("main")) {
log("Started") // [main] Started
val v1 = async(CoroutineName("c1")) {
delay(500)
log("Running async") // [c1] Running async
42
}
launch(CoroutineName("c2")) {
delay(1000)
log("Running launch") // [c2] Running launch
}
log("The answer is ${v1.await()}") // [main] The answer is 42
}
모든 자식은 빌더의 인자에서 정의된 특정 컨텍스트를 가질 수 있다.
인자로 전달된 컨텍스트는 부모로부터 상속받은 컨텍스트를 대체한다.
defaultContext + parentContext + childContext
코루틴 컨텍스트를 계산하는 간단한 공식은 위와 같다.
새로운 원소가 같은 키를 가진 이전 원소를 대체하므로, 자식의 컨텍스트는 부모로부터 상속받은 컨텍스트 중 같은 키를 가진 원소를 대체한다.
디폴트 원소는 어디서도 키가 지정되지 않았을 때만 사용된다.
현재 디폴트로 설정되는 원소는 continuationInterceptor가 설정되지 않았을 때 사용되는 Dispatchers.Default이며, 애플리케이션이 디버그 모드일 때는 CoroutineId도 디폴트로 설정된다.
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
suspend fun printName() {
println(coroutineContext[CoroutineName]?.name)
}
suspend fun main() = withContext(CoroutineName("Outer")) {
printName() // Outer
launch(CoroutineName("Inner")) {
printName() // Inner
}
delay(10)
printName() // Outer
}
중단 함수 사이에 전달되는 컨티뉴에이션 객체는 컨텍스트를 참조하고 있다. 따라서 중단 함수에서 부모의 컨텍스트에 접근하는 것이 가능하다.
coroutineContext 프로퍼티는 모든 중단 스코프에서 사용 가능하며, 이를 통해 컨텍스트에 접근할 수 있다.