=================
== The Archive ==
=================

[Kotlin Coroutines] 7장. 코루틴 컨텍스트

개요

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

CoroutineContext 인터페이스

1
2
3
4
5
6
7
8
9
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 에서 원소 찾기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    val name = "A name"
    val ctx: CoroutineContext = CoroutineName(name)

    val coroutineName: CoroutineName? = ctx[CoroutineName]
    // or ctx.get(CoroutineName)
    assert(coroutineName?.name == name) // A name
    val job: Job? = ctx[Job] // or ctx.get(Job)
    assert(job == null) // null
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public data class CoroutineName(
    /**
     * User-defined coroutine name.
     */
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    /**
     * Key for [CoroutineName] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineName>

    /**
     * Returns a string representation of the object.
     */
    override fun toString(): String = "CoroutineName($name)"
}
1
2
3
4
5
public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job>

컨텍스트 더하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fun main() {
    val name1 = "Name1"
    val ctx1: CoroutineContext = CoroutineName(name1)
    assert(ctx1[CoroutineName]?.name == name1) // Name1
    assert(ctx1[Job]?.isActive == null) // null

    val ctx2: CoroutineContext = Job()
    assert(ctx2[CoroutineName]?.name == null) // null
    assert(ctx2[Job]?.isActive == true) // true, because "Active"
    // is the default state of a job created this way

    val ctx3 = ctx1 + ctx2
    assert(ctx3[CoroutineName]?.name == name1) // Name1
    assert(ctx3[Job]?.isActive == true) // true
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    val name1 = "Name1"
    val ctx1: CoroutineContext = CoroutineName(name1)
    assert(ctx1[CoroutineName]?.name == name1) // Name1, 'CoroutineName' 가 키

    val name2 = "Name2"
    val ctx2: CoroutineContext = CoroutineName(name2)
    assert(ctx2[CoroutineName]?.name == name2) // Name2, 'CoroutineName' 가 키

    val ctx3 = ctx1 + ctx2
    assert(ctx3[CoroutineName]?.name == name2) // Name2
}

비어 있는 코루틴 컨텍스트

1
2
3
4
5
6
7
8
9
fun main() {
    val empty: CoroutineContext = EmptyCoroutineContext
    assert(empty[CoroutineName] == null) // null
    assert(empty[Job] == null) // null

    val name1 = "Name1"
    val ctxName = empty + CoroutineName(name1) + empty
    assert(ctxName[CoroutineName] == CoroutineName(name1)) // CoroutineName(Name1)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SinceKotlin("1.3")
public object EmptyCoroutineContext : CoroutineContext, Serializable {
    private const val serialVersionUID: Long = 0
    private fun readResolve(): Any = EmptyCoroutineContext

    public override fun <E : Element> get(key: Key<E>): E? = null
    public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
    public override fun plus(context: CoroutineContext): CoroutineContext = context
    public override fun minusKey(key: Key<*>): CoroutineContext = this
    public override fun hashCode(): Int = 0
    public override fun toString(): String = "EmptyCoroutineContext"
}

원소 제거

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fun main() {
    val name1 = "Name1"
    val ctx = CoroutineName(name1) + Job()
    assert(ctx[CoroutineName]?.name == name1) // Name1
    assert(ctx[Job]?.isActive == true) // true

    val ctx2 = ctx.minusKey(CoroutineName)
    assert(ctx2[CoroutineName]?.name == null) // null
    assert(ctx2[Job]?.isActive == true) // true

    val name2 = "Name2"
    val ctx3 =
        (ctx + CoroutineName(name2))
            .minusKey(CoroutineName)
    assert(ctx3[CoroutineName]?.name == null) // null
    assert(ctx3[Job]?.isActive == true) // true
}

컨텍스트 폴딩

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fun main() {
    val ctx = CoroutineName("Name1") + Job()

    ctx.fold("") { acc, element -> "$acc$element " }
        .also(::println) // CoroutineName(Name1) JobImpl{Active}@2b71fc7e

    val empty = emptyList<CoroutineContext>()
    ctx.fold(empty) { acc, element -> acc + element }
        .joinToString()
        .also(::println) // CoroutineName(Name1), JobImpl{Active}@2b71fc7e
}

코루틴 컨텍스트와 빌더

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private 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
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private 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(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
    }

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private suspend fun printName() {
    println(coroutineContext[CoroutineName]?.name)
}

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

컨텍스트를 개별적으로 생성하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class CounterContext(
    private val name: String,
) : CoroutineContext.Element {
    override val key: CoroutineContext.Key<*> = Key
    private var nextNumber = 0

    fun printNext() {
        println("$name: $nextNumber")
        nextNumber++
    }

    companion object Key : CoroutineContext.Key<CounterContext>
}

private suspend fun printNext() {
    coroutineContext[CounterContext]?.printNext()
}

suspend fun main(): Unit =
    withContext(CounterContext("Outer")) {
        printNext() // Outer: 0
        launch {
            printNext() // Outer: 1
            launch {
                printNext() // Outer: 2
            }
            launch(CounterContext("Inner")) {
                printNext() // Inner: 0
                printNext() // Inner: 1
                launch {
                    printNext() // Inner: 2
                }
            }
        }
        printNext() // Outer: 3
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private data class User(val id: String, val name: String)

private abstract class UuidProviderContext :
    CoroutineContext.Element {
    abstract fun nextUuid(): String

    override val key: CoroutineContext.Key<*> = Key

    companion object Key :
        CoroutineContext.Key<UuidProviderContext>
}

private class RealUuidProviderContext : UuidProviderContext() {
    override fun nextUuid(): String = UUID.randomUUID().toString()
}

private class FakeUuidProviderContext(
    private val fakeUuid: String,
) : UuidProviderContext() {
    override fun nextUuid(): String = fakeUuid
}

private suspend fun nextUuid(): String =
    checkNotNull(coroutineContext[UuidProviderContext]) {
        "UuidProviderContext not present"
    }
        .nextUuid()

// function under test
private suspend fun makeUser(name: String) =
    User(
        id = nextUuid(),
        name = name,
    )

suspend fun main() {
    // production case
    withContext(RealUuidProviderContext()) {
        println(makeUser("Michał"))
        // e.g. User(id=d260482a-..., name=Michał)
    }

    // test case
    withContext(FakeUuidProviderContext("FAKE_UUID")) {
        val user = makeUser("Michał")
        println(user) // User(id=FAKE_UUID, name=Michał)
        assert(User("FAKE_UUID", "Michał") == user)
    }
}

요약