Flow의 컨텍스트 보존(context preservation)

홍성덕·2024년 11월 12일

1. Flow의 컨텍스트 보존

Flow의 실행은 수집자의 컨텍스트를 그대로 사용해서 실행한다. Flow의 collect 함수를 호출하면, Flow의 실행은 collect를 호출한 코루틴의 컨텍스트에서 이루어진다. 이렇게 Flow의 실행이 수집자의 컨텍스트를 보존하는 특성을 컨텍스트 보존(context preservation)이라고 한다.

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(3000)
        println("Emitting $i: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple().collect { value ->
        delay(2000)
        println("Collecting $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
    }
}

/* 결과 :
(3초 후)
Emitting 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
(2초 후)
Collecting 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
(3초 후)
Emitting 2: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
(2초 후)
Collecting 2: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
(3초 후)
Emitting 3: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
(2초 후)
Collecting 3: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@256216b3, BlockingEventLoop@2a18f23c]
/*

위의 코드를 보면 simple Flow가 실행되는 스레드가 메인 스레드인 것을 확인할 수 있다. simple Flow가 collect를 호출한 코루틴 컨텍스트를 보존하여 실행되기 때문에, runBlocking 함수로 생성된 코루틴의 컨텍스트를 따른다.

runBlocking은 현재 스레드를 차단하며 코루틴을 실행하는 함수이고, runBlocking 코루틴이 메인 스레드에서 실행되었기 때문에, Flow 내부에서 메인 스레드에 관련된 로직이 없음에도 불구하고 메인 스레드에서 실행되는 것이다.

이렇게 컨텍스트 보존이라는 특성은 불필요한 컨텍스트 전환을 피할 수 있어 효율적이고, 코드가 순차적으로 동작하여 코드가 동작하는 데 예측 가능성이 높아진다는 장점이 있다.
순차적으로 동작한다는 것은 생산 -> (중간 연산) -> 소비 -> 다음 데이터 생산 (이후 같은 과정 반복) 순으로 동작한다는 의미이다. 이렇게 순차적으로 동작하는 이유는 같은 결과 출력에서도 알 수 있듯이 생산과 소비가 같은 코루틴에서 이루어지고 있기 때문이다.

2. withContext를 사용할 때 겪을 수 있는 함정

만약 CPU를 많이 사용해야 하는 오래 걸리는 작업은 Dispatchers.Default 컨텍스트에서 실행되어야 할 수 있고, UI를 업데이트하는 코드는 Dispatchers.Main 컨텍스트에서 실행되어야 할 수 있다.
withContext 함수는 코루틴의 컨텍스트를 변경하는데 사용되지만, flow 빌더는 컨텍스트 보존이라는 특성을 준수해야 하기 때문에 다른 컨텍스트에서 값을 방출하는 것을 허용하지 않는다.

fun simple(): Flow<Int> = flow {
    // The WRONG way to change context for CPU-consuming code in flow builder
    withContext(Dispatchers.Default) {
        for (i in 1..3) {
            Thread.sleep(100) // pretend we are computing it in CPU-consuming way
            emit(i) // emit next value
        }
    }
}

fun main() = runBlocking<Unit> {
    simple().collect { value -> println(value) }
}

/* 결과 :
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
		Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],
		but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default].
		Please refer to 'flow' documentation or use 'flowOn' instead
	at ...
/*

위 코드를 실행하면 오류가 발생한다. Flow의 수집은 [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323]라는 코루틴 컨텍스트에서 일어나지만, 값의 방출은 [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, Dispatchers.Default]라는 코루틴 컨텍스트에 일어났다는 오류 메시지이다.

수집과 방출이 동일한 컨텍스트에서 일어나야 한다는 컨텍스트 보존이라는 특성을 준수하지 않았기 때문에 오류가 발생하는 것이다.
그리고 오류 메시지에 flowOn을 대신 사용하라는 메시지도 있다. flowOn을 사용하는 것이 컨텍스트를 변경하는 방법이라는 힌트를 얻을 수 있다.

3. flowOn 연산자

위에서 발생한 Exception을 해결하기 위해 컨텍스트 보존 특성을 준수하는 방법도 존재하지만, flowOn 연산자를 사용하는 방법도 있다. flowOn 연산자는 Flow가 실행되는 컨텍스트를 전달된 컨텍스트로 변경하는 함수이다.

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(1000)
        println("Emitting $i: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .map {
            delay(2000)
            println("Mapping $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it * it
        }
        .flowOn(Dispatchers.IO)
        .collect { value ->
            delay(1000)
            println("Collected $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        }
}
/* 결과 :
Emitting 1: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Mapping 1: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Emitting 2: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Collected 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@668bc3d5, BlockingEventLoop@3cda1055]
Mapping 2: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Collected 4: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@668bc3d5, BlockingEventLoop@3cda1055]
Emitting 3: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Mapping 3: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@519ea6a0, Dispatchers.IO]
Collected 9: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@668bc3d5, BlockingEventLoop@3cda1055]
*/
  1. flowOn 연산자는 업스트림의 컨텍스트를 변경하고 다운스트림에 영향을 주지 않는다.
    위 코드의 simple Flow와 map 함수는 IO 스레드의 coroutine#2에서 실행되지만, collect는 flowOn의 영향을 받지 않으므로 수집자의 기존 컨텍스트를 따라 메인 스레드의 coroutine#1에서 실행된다.

  2. 여기서 한가지 더 주목할 점은 flowOn 연산자에서 CoroutineDispatcher를 변경할 때 Flow의 기본적인 순차처리 특성을 따르지 않는다는 점이다.
    일반적인 경우라면 Emitting 1 -> Mapping 1 -> Collected 1 순으로 출력되고 나머지 값도 차례대로 출력되어야 할 것이다. 하지만 결과 출력에서도 볼 수 있듯이 순차적으로 처리되고 있지 않다.
    생산이 IO 스레드의 coroutine#2에서 진행되는 동시에 메인 스레드의 #coroutine1에서 소비가 일어나기 때문이다. 이처럼 flowOn 연산자는 CoroutineDispatcher를 변경할 때 업스트림 Flow를 위한 다른 코루틴을 생성한다.

그렇다면 여기서 궁금한 점이 하나 생겼다. CoroutineDispatcher를 변경하는 게 아닌 다른 CoroutineContext를 변경할 때는 어떨까? 위 코드의 주요 부분을 다음과 같이 바꿔보았다.

fun main() = runBlocking<Unit> {
    simple()
        .map {
            delay(2000)
            println("Mapping $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it * it
        }
        .flowOn(CoroutineExceptionHandler { _, throwable -> println("Caught $throwable") })
        .collect { value ->
            delay(1000)
            println("Collected $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        }
}
    
/* 결과 :
Emitting 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Mapping 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Collected 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, BlockingEventLoop@48140564]
Emitting 2: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Mapping 2: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Collected 4: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, BlockingEventLoop@48140564]
Emitting 3: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Mapping 3: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, TestKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@1d251891, BlockingEventLoop@48140564]
Collected 9: [main @coroutine#1], [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@27f674d, BlockingEventLoop@48140564]
*/

Emitting과 Mapping에서 CoroutineExceptionHandler가 새로운 컨텍스트 요소로 추가되었다. 하지만 출력은 순차적으로 출력되었다. CoroutineDispatcher가 아닌 다른 컨텍스트 요소를 변경할 때는 순차적으로 처리된다는 것을 알게 되었다. 새로운 코루틴을 생성하는 것이 아닌 하나의 코루틴에서 생산과 소비가 모두 일어나기 때문이다.

정리하자면, flowOn을 사용할 때 전달된 코루틴 컨텍스트로 업스트림의 컨텍스트를 변경하는 것이기 때문에 컨텍스트 보존은 지켜지지 않는다. 하지만 CoroutineDispatcher를 변경하는 게 아니라면 생산과 소비가 하나의 코루틴에서 이루어지기 때문에 순차처리 특성은 유지된다.


  1. flowOn의 또다른 특징은 여러 개의 flowOn 연산자가 사용될 때 동일한 컨텍스트 키를 가진 경우, 첫 번째 flowOn 연산자의 컨텍스트 요소가 두 번째 flowOn 연산자의 요소보다 우선시된다는 점이다.
flow.map { ... } // IO에서 실행
    .flowOn(Dispatchers.IO) // 이것이 우선시
    .flowOn(Dispatchers.Default)

위 코드에서도 Default가 아닌 IO를 우선시하여 map 함수는 IO 스레드에서 실행된다.


4. flowOn에 대한 여러가지 테스트

이전 파트에서 작성한 특징들에 대해 좀 더 확실히 알기 위해서 몇 가지 테스트를 좀 더 진행해보았다. 먼저 특징 2번에 대해 더 자세히 알기 위한 테스트이다.

fun main() = runBlocking<Unit> {
    simple()
        .map {
            delay(2000)
            println("Mapping $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it * it
        }
        .flowOn(Dispatchers.IO)
        .filter {
            delay(2000)
            println("Filtering $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it < 10
        }
        .flowOn(Dispatchers.IO)
        .collect { value ->
            delay(1000)
            println("Collected $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        }
}
/*
Emitting 1: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Mapping 1: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Filtering 1: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Emitting 2: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Collected 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@59a6e353, BlockingEventLoop@7a0ac6e3]
Mapping 2: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Filtering 4: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Collected 4: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@59a6e353, BlockingEventLoop@7a0ac6e3]
Emitting 3: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Mapping 3: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Filtering 9: [DefaultDispatcher-worker-1 @coroutine#2], [CoroutineId(2), "coroutine#2":ProducerCoroutine{Active}@26cca0c, Dispatchers.IO]
Collected 9: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@59a6e353, BlockingEventLoop@7a0ac6e3]
*/

여기서 확인할 수 있는 중요한 점은 flowOn은 업스트림 Flow의 CoroutineDispatcher를 변경해야 할 때만 새로운 코루틴을 생성한다는 점이다. 예시 코드의 flowOn에서 Dispatchers.IO가 두 곳에서 사용되었는데, CoroutineDispatcher가 변경되지 않았기 때문에 filter 블록이 실행되는 코루틴은 simple Flow와 map 블록이 실행되는 코루틴과 동일하다. 그래서 Collect를 제외하고 Filtering까지 순차적으로 처리되는 것이다.

fun main() = runBlocking<Unit> {
    val singleThreadContext = newSingleThreadContext("SingleThread")
    val singleThreadContext2 = newSingleThreadContext("SingleThread2")
    simple()
        .map {
            delay(2000)
            println("Mapping $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it * it
        }
        .flowOn(singleThreadContext)
        .filter {
            delay(2000)
            println("Filtering $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it < 10
        }
        .flowOn(singleThreadContext2)
        .collect { value ->
            delay(1000)
            println("Collected $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        }
}
/*
Emitting 1: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 1]]
Mapping 1: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 2]]
Emitting 2: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 3]]
Filtering 1: [SingleThread2 @coroutine#2], [CoroutineId(2), "coroutine#2":ScopeCoroutine{Active}@429fcd52, java.util.concurrent.ScheduledThreadPoolExecutor@4f4a7090[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 2]]
Mapping 2: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 4]]
Collected 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@511baa65, BlockingEventLoop@340f438e]
Emitting 3: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 5]]
Filtering 4: [SingleThread2 @coroutine#2], [CoroutineId(2), "coroutine#2":ScopeCoroutine{Active}@429fcd52, java.util.concurrent.ScheduledThreadPoolExecutor@4f4a7090[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 4]]
Collected 4: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@511baa65, BlockingEventLoop@340f438e]
Mapping 3: [SingleThread @coroutine#3], [CoroutineId(3), "coroutine#3":ProducerCoroutine{Active}@148f60a8, java.util.concurrent.ScheduledThreadPoolExecutor@5b1d2887[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 6]]
Filtering 9: [SingleThread2 @coroutine#2], [CoroutineId(2), "coroutine#2":ScopeCoroutine{Active}@429fcd52, java.util.concurrent.ScheduledThreadPoolExecutor@4f4a7090[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 6]]
Collected 9: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@511baa65, BlockingEventLoop@340f438e]
*/

하지만 위 코드에서는 CoroutineDispatcher가 변경되었기 때문에 simple Flow, map이 실행되는 코루틴과 filter가 실행되는 코루틴이 다르다. 똑같은 싱글 스레드 컨텍스트이지만 엄연히 다른 CoroutineDispatcher 객체이기 때문이다. 그래서 순차적으로 동작하지 않는다. 싱글 스레드인지 멀티 스레드인지가 아닌 업스트림 Flow의 CoroutineDispatcher가 변경되는지가 새로운 코루틴을 생성하는 기준이 디는 것이다.


이전파트 flowOn 연산자 특징 3번에서, 동일한 컨텍스트 키를 가진 경우 첫 번째 flowOn 연산자의 컨텍스트 요소가 두 번째 flowOn 연산자의 요소보다 우선시된다는 점을 언급하였다. 이에 대한 테스트를 한가지 더 진행하였다.

fun main() = runBlocking<Unit> {
    val singleThreadContext = newSingleThreadContext("MyThread")
    simple()
        .map {
            delay(2000)
            println("Mapping $it: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
            it * it
        }
        .flowOn(Dispatchers.Default)
        .flowOn(CoroutineName("My코루틴") + singleThreadContext)
        .collect { value ->
            delay(1000)
            println("Collected $value: [${Thread.currentThread().name}], ${currentCoroutineContext()}")
        }
}
/*
Emitting 1: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Mapping 1: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Emitting 2: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Collected 1: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@3796751b, BlockingEventLoop@67b64c45]
Mapping 2: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Collected 4: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@3796751b, BlockingEventLoop@67b64c45]
Emitting 3: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Mapping 3: [DefaultDispatcher-worker-1 @My코루틴#2], [CoroutineName(My코루틴), CoroutineId(2), "My코루틴#2":ProducerCoroutine{Active}@73274580, Dispatchers.Default]
Collected 9: [main @coroutine#1], [CoroutineId(1), "coroutine#1":ScopeCoroutine{Active}@3796751b, BlockingEventLoop@67b64c45]
*/

이렇게 flowOn이 중복되어서(겹쳐서) 사용되었을 때, 동일한 컨텍스트 키를 가진 경우 첫 번째 flowOn 연산자의 컨텍스트 요소가 두 번째 flowOn 연산자의 요소보다 우선시되므로, 여기서 simple Flow와 map 함수 블록은 Dispatchers.Default 환경에서 실행된다. singleThreadContext는 무시된다.

하지만 CoroutineName("My코루틴")은 기존의 동일한 컨텍스트 키를 가진 적이 없기 때문에, 즉 기존에 CoroutineName 컨텍스트 요소를 지정해준 적이 없기 때문에 simple Flow와 map 함수 블록이 실행되는 컨텍스트에 영향을 준다.


참고자료

profile
안드로이드 주니어 개발자

0개의 댓글