관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 7. 코루틴 컨텍스트 본문

책/코틀린 코루틴

[코틀린 코루틴] 7. 코루틴 컨텍스트

참깨빵위에참깨빵 2024. 2. 17. 01:56
728x90
반응형

launch의 정의를 보면 첫 파라미터가 coroutineContext인 걸 알 수 있다.

 

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

 

마지막 인자의 리시버도 CoroutineScope다. 이것은 중요한 개념 같으니 시그니처를 확인한다.

 

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
@SinceKotlin("1.3")
public interface Continuation<in T> {
    public val context: CoroutineContext

    public fun resumeWith(result: Result<T>)
}

 

Continuation도 CoroutineContext를 갖고 있다. 코루틴에서 가장 중요한 요소들이 모두 갖고 있는데 이건 무엇인가?

 

CoroutineContext 인터페이스

 

CoroutineContext는 원소나 원소의 집합을 나타내는 인터페이스다. CoroutineName, CoroutineDispatcher 등 Element 객체들이 인덱싱된 집합이란 점에서 Map, Set 같은 컬렉션과 비슷하다. 특이한 건 각 Element도 CoroutineContext라는 것이다.

 

컨텍스트에서 모든 원소는 식별 가능한 유일한 key를 갖고 있으며 각 key는 주소로 비교된다. CoroutineName이나 Job은 CoroutineContext 인터페이스를 구현한 CoroutineContext.Element를 구현한다

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext

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
}

 

CoroutineContext에서 원소 찾기

 

CoroutineContext는 컬렉션과 비슷하게 get으로 유일한 키를 가진 원소를 찾을 수 있다. 대괄호를 쓰는 것도 가능하다.

원소가 존재하지 않으면 null을 리턴한다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext

fun main() {
    val ctx: CoroutineContext = CoroutineName("A name")
    val coroutineName: CoroutineName? = ctx[CoroutineName] // ctx.get(CoroutineName)
    println(coroutineName?.name)

    val job: Job? = ctx[Job]
    println(job)
}

// A name
// null

 

컨텍스트 더하기

 

CoroutineContext의 유용한 기능은 2개의 CoroutineContext를 합쳐서 하나의 CoroutineContext로 만드는 것이다.

다른 키를 가진 두 원소를 더해서 만들어진 컨텍스트는 키 2개를 모두 갖고 있다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext

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
    // 빌더를 통해 생성되는 Job의 기본 상태가 Active가 되어 true가 된다

    val ctx3 = ctx1 + ctx2
    println(ctx3[CoroutineName]?.name)  // Name1
    println(ctx3[Job]?.isActive)        // true
}

 

비어 있는 코루틴 컨텍스트

 

앞서 말했듯 CoroutineContext는 컬렉션과 비슷해서 빈 컨텍스트도 만들 수 있다. 빈 컨텍스트는 원소가 없기 때문에 다른 컨텍스트에 더해도 변화가 없다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

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)
}

 

원소 제거

 

minusKey()에 키를 넣어서 특정 원소를 컨텍스트에서 제거할 수 있다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Job

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
}

 

컨텍스트 폴딩

 

fold는 컬렉션의 fold와 비슷하게 아래를 요구한다.

 

  • 누산기의 1번째 값
  • 누산기의 현재 상태와 현재 실행되고 있는 원소로 누산기의 다음 상태를 계산할 연산

 

코루틴 컨텍스트, 빌더

 

CoroutineContext는 코루틴의 데이터를 저장, 전달하는 방법이다. 부모-자식 관계의 영향으로 인해 부모는 컨텍스트를 자식에게 전달하고, 자식은 부모로부터 컨텍스트를 상속받는다.

모든 자식은 코루틴 빌더의 인자에 정의된 컨텍스트를 가질 수 있다. 이 때 인자로 전달된 컨텍스트는 부모로부터 상속받은 컨텍스트를 대체한다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking(CoroutineName("main")) {
    log("Started")
    val v1 = async(CoroutineName("c1")) {
        delay(500)
        log("Running async")
        42
    }
    launch(CoroutineName("c2")) {
        delay(1000)
        log("Running launch")
    }
    log("answer : ${v1.await()}")
}

fun CoroutineScope.log(msg: String) {
    val name = coroutineContext[CoroutineName]?.name
    println("[$name] $msg")
}

// [main] Started
// [c1] Running async
// [main] answer : 42
// [c2] Running launch

 

새 원소가 같은 키를 가진 이전 원소를 대체하기 때문에, 자식의 컨텍스트는 부모한테서 상속받은 컨텍스트 중 같은 키를 가진 원소를 대체한다.

 

중단 함수에서 컨텍스트에 접근하기

 

CoroutineScope는 컨텍스트 접근 시 사용하는 coroutineContext 프로퍼티를 갖고 있다.

일반적인 중단 함수에서 컨텍스트에 접근하는 방법은 중단 함수 사이에 전달되는 Continuation 객체를 사용해 중단 함수에서 부모의 컨텍스트에 접근하는 것이다.

coroutineContext 프로퍼티는 모든 중단 스코프에서 쓸 수 있고, 이를 통해 컨텍스트에 접근할 수 있다.

 

import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.coroutineContext

suspend fun main() = withContext(CoroutineName("Outer")) {
    printName()
    launch(CoroutineName("Inner")) {
        printName()
    }
    delay(10)
    printName()
}

suspend fun printName() = println(coroutineContext[CoroutineName]?.name)

// Outer
// Inner
// Outer

 

반응형
Comments