ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Coroutine] 코루틴 학습 - 9 (Coroutine scope function)
    Java/Kotlin 2022. 5. 7. 10:05
    반응형

    GlobalScope

    • 두 개 이상의 일시중단 함수를 병렬적으로 수행하기 위한 가장 손쉬운 방법은 GlobalScope.async()를 호출하는 것이다.
    • 하지만 GlobalScope는 지양해야하는데, 우선 GlobalScope의 정의를 먼저 살펴보면..
    public object GlobalScope : CoroutineScope {
        override val coroutineContext: CoroutineContext
            get() = EmptyCoroutineContext
    }

     

    • GlobalScope의 정의를 살펴보면 coroutineContext가 EmptyCoroutineContext이다.
    • 코루틴 스코프 내에서 GlobalScope를 사용하면 부모 코루틴의 컨텍스트를 덮어쓰게 되고, 아래와 같은 문제가 발생한다.
      • 부모 코루틴이 취소되어도 GlobalScope는 취소되지 않는다.
      • 부모 코루틴의 스코프를 상속받지 않으므로 항상 default dispatcher로 실행되고, 부모 코루틴의 컨텍스트를 따르지 않는다.
      • 이는 잠재적인 메모리 누수와 불필요한 계산을 야기시킨다.
      • 코루틴을 단위테스트하는 툴이 제대로 동작하지 않고, 테스트하기도 어렵다.

     

    coroutineScope

    • courinteScope는 스코프를 시작하는 일시중단 함수를 제공한다.
    • 전달된 argument function에 의해 생성된 값을 반환한다.
    public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
    • 새로운 코루틴을 만들지만, 새로운 코루틴이 완료될 때까지 이전 코루틴을 일시중단 하므로 동시에 프로세스를 실행하지 않는다.
    fun main() = runBlocking {
        val a = coroutineScope {
            delay(2000)
            10
        }
        println("a is calculated")
    
        val b = coroutineScope {
            delay(2000)
            20
        }
        println(a)
        println(b)
    }
    // (2 sec)
    // a is calculated
    // (2 sec)
    // 10
    // 20
    • coroutineContext에서 바깥 스코프로부터 스코프를 상속받고 싶다면 context의 Job을 오버라이드하면 된다. 이 방법은 아래와 같은 효과를 가지게 한다.
      • 부모로부터 컨텍스트를 상속받는다.
      • 모든 자식 코루틴이 완료될 때까지 기다린다.
      • 보무 코루틴이 취소되었을 때, 모든 자식 코루틴 또한 취소된다.
    suspend fun longTask() = coroutineScope {
        launch {
            delay(1000)
            val name = coroutineContext[CoroutineName]?.name
            println("[$name] Finished task 1")
        }
    
        launch {
            delay(2000)
            val name = coroutineContext[CoroutineName]?.name
            println("[$name] Finished task 2")
        }
    }
    
    fun main() = runBlocking(CoroutineName("Parent")) {
        println("Before")
        longTask()
        println("After")
    }
    // Before
    // [Parent] Finished task 1
    // [Parent] Finished task 2
    // After
    • 위 코드를 살펴보면 "After"가 마지막에 출력되고 있는 것을 확인할 수 있는데, coroutineScope는 자식 코루틴이 끝날 때까지 완료되지 않기 때문이다.
    • CoroutineName이 자식 코루틴으로 상속되어 코루틴 이름이 출력되는 것을 확인할 수 있다.
    suspend fun longTask2() = coroutineScope {
        launch {
            delay(1000)
            val name = coroutineContext[CoroutineName]?.name
            println("[$name] Finished task 1")
        }
    
        launch {
            delay(2000)
            val name = coroutineContext[CoroutineName]?.name
            println("[$name] Finished task 2")
        }
    }
    
    fun main() = runBlocking {
        val job = launch(CoroutineName("Parent")) {
            longTask2()
        }
        delay(1500)
        job.cancel()
    }
    // [Parent] Finished task 1
    • 위 코드에서는 부모 코루틴이 종료되었을 때, 자식 코루틴도 함께 종료되는 것을 확인할 수 있다.

     이를 바탕으로 비동기로 병렬 처리를 하는 코드를 아래 예제와 같이 작성해볼 수 있다.

    suspend fun getUserProfile(): Example_42.User = coroutineScope {
        val userName = async {
            delay(2000)
            "Hello"
        }
    
        val profileImagePath = async {
            delay(3000)
            "/hello/profile-image"
        }
    
        Example_42.User(
            name = userName.await(),
            profileImagePath = profileImagePath.await()
        )
    }
    
    fun main() = runBlocking {
        val userProfile = async(CoroutineName("Parent")) {
            getUserProfile()
        }
        println(userProfile.await())
    }
    // (3 sec)
    // User(name=Hello, profileImagePath=/hello/profile-image)

     

    Coroutine scope functions

     Coroutine builder와 Coroutine scope functinos의 차이는 아래의 테이블과 같다.

    Coroutine builders (runBlocking 제외) Coroutine scope functions
    launch,
    async,
    produce
    coruotineScope,
    supervisorScope,
    withContext,
    withTimeout
    CoroutineScope의 확장 함수 일시중단 함수
    CoroutineScope의 리시버로부터 coruotine context를 받는다. 일시중단 함수의 continuation으로부터 coroutine context를 받는다.
    예외는 Job을 통해 부모로 전파된다. 익셉션 발생 시, 일반 함수처럼 동작한다.
    비동기 방식으로 코루틴을 시작한다. 제자리에서 실행되는 코루틴을 시작한다.

     여기서 추가적으로 runBlocking과 coroutine scope function을 비교하면 다음과 같다.

    • runBlocking은 해당 본문이 호출하고 값을 반환한다.
    • runBlocking은 함수를 블로킹하는 반면에, coroutine scope functions은 일시중단 시킨다.
    • runBlocking은 코루틴의 최상단에 위치해야 하고, coroutine scope function은 중간에 있어야 한다.

     

    withContext

    • withContext는 추가적으로 스코프를 변경시킨 coroutineScope와 유사하다.
    • 이 함수에 대한 인수로 제공된 컨텐스트는 상위 스코프의 컨텍스트에 추가된다.
    fun CoroutineScope.log(text: String) {
        val name = this.coroutineContext[CoroutineName]?.name
        println("[$name] $text")
    }
    
    fun main() = runBlocking(CoroutineName("Parent")) {
        log("Before")
    
        withContext(CoroutineName("Child 1")) {
            delay(1000)
            log("Hello 1")
        }
    
        withContext(CoroutineName("Child 2")) {
            delay(1000)
            log("Hello 2")
        }
    
        log("After")
    }
    // [Parent] Before
    // [Child 1] Hello 1
    // [Child 2] Hello 2
    // [Parent] After

     

    supervisorScope

    • coroutineScope처럼 바깥 스코프로부터 상속을 받은 CoroutineScope를 생성하고, 이 범위로 지정된 일시중단 블록을 호출한다.
    • coroutineScope와의 차이점은 Job의 컨텍스트를 SupervisorJob으로 오버라이드 한다는 점이다. 따라서 자식 코루틴에서 발생한 예외로 인해 취소되지 않는다.
    • 서로 독립된 여러 테스크를 동시에 실행할 때 주로 사용한다.
    fun main() = runBlocking {
        println("Before")
        supervisorScope {
            launch {
                delay(1000)
                throw Error()
            }
    
            launch {
                delay(2000)
                println("Done")
            }
        }
        println("After")
    }
    // Before
    // (1 sec)
    // Exception...
    // (1 sec)
    // Done
    // After

     

     

    만약 supervisorScope 대신 withContext(SupervisorJob())을 사용하면 같은 기능으로써 대체가 가능할까?

    fun main() = runBlocking { 
        println("Before")
        withContext(SupervisorJob()) {
            launch { 
                delay(1000)
                throw Error()
            }
            
            launch { 
                delay(2000)
                println("Done")
            }
        }
        println("After")
    }
    // Before
    // (1 sec)
    // Exception...

     withContext(SupervisorJob())은 여전히 일반적인 Job()을 사용하고, SupervisorJob()은 부모가 된다. 그 결과로, 자식 코루틴 중 하나가 예외를 발생시켰을 때, 다른 자식들도 함께 취소된다. withContext 또한 예외를 던진다.

     

    withTimeout

    • scope를 생성하고 값을 리턴한다.
    • coroutineScope와 차이점은 본문의 실행을 위한 제한 시간을 추가적으로 설정할 수 있다는 점이다.
    • 만약 실행 시간이 너무 길어진다면, 본문의 실행을 취소시키고 TimeoutCancellationException을 던진다.
    • TimeoutCancellationException은 CancellationException의 서브타입이다.
    suspend fun test(): Int = withTimeout(1500) {
        delay(1000)
        println("Still thinking")
        delay(1000)
        println("Done")
        42
    }
    
    suspend fun main(): Unit = coroutineScope {
        try {
            test()
        } catch (e: TimeoutCancellationException) {
            println("Cancelled")
        }
        delay(1000)
    }
    // Still thinking
    // Cancelled
    • withTimeout() 함수는 테스트에서 유용하다. 어떤 기능이 수행하는데 시간이 더 걸리는지, 덜 걸리는지 테스트하는데 사용할 수 있다.
    • runBlockingTest 내부에서 사용된다면, 가상 시간으로 계산이 되어 수행한다.
    • 설정한 타임아웃 시간이 초과되었을 때, CancellationException의 서브타입인 TimeoutCancellationException을 던지므로 해당 코루틴만 취소되고, 그 부모 코루틴에는 영향을 주지 않는다.
    class Test {
    
        @ExperimentalCoroutinesApi
        @Test
        fun testTime1() = runTest {
            withTimeout(10000) {
                delay(9000) // virtual time
            }
        }
    
        @ExperimentalCoroutinesApi
        @Test
        fun testTime2() = runTest {
            shouldThrow<TimeoutCancellationException> {
                withTimeout(1000) {
                    delay(1100) // virtual time
                }
            }
        }
    
        @Test
        fun testTime3() = runBlocking {
            withTimeout(1000) {
                delay(900)  // 실제 900ms를 기다린다..
            }
        }
    }
    suspend fun main(): Unit = coroutineScope {
        launch {
            launch {
                delay(2000)
                println("Will not be printed")
            }
            try {
                withTimeout(1000) {
                    delay(1500)
                }
            } catch (e: TimeoutCancellationException) {
                println("Thrown TimeoutCancellationException")
            }
        }
        launch {
            delay(2000)
            println("Done")
        }
    }
    // Thrown TimeoutCancellationException
    // Done
    // Will not be printed

     


    참고자료

    https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837

    반응형

    댓글

Designed by Tistory.