ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코루틴(Coroutine) - 입문
    Android(+ Kotlin) 2020. 1. 22. 12:09

     

     

    2020/01/16 - [Android(+ Kotlin)] - 코루틴(Coroutine) 기본개념

     

    코루틴(Coroutine) 기본개념

    코루틴은 Kotlin언어를 개발한 jetbrains에서 만들어졌다. Java에서는 사용할 수 없다. 서브루틴(subroutine) 먼저 서브루틴 개념이 필요하다, 하나의 함수를 예를 들어 파라미터를 받고 시작해서 끝 지점에서 종..

    charko.tistory.com

    먼저 위 기본개념을 익히고

     

    https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html

     

    Composing Suspending Functions - Kotlin Programming Language

     

    kotlinlang.org

    (아래 링크의 내용을 참고하여 작성하였습니다.)

    Composing Suspending Functions

    - Sequential by default

    suspend fun doSomethingUsefulOne(): Int {
        delay(1000L)
        return 13
    }
    
    suspend fun doSomethingUsefulTwo(): Int {
        delay(1000L)
        return 29
    }

    delay로 인해 suspend 키워드가 필요하다. (언제든 엄추고 재 시작 가능한 함수로 변경)

     

    fun main() = runBlocking {
        val time = measureTimeMillis {
            val one = doSomethingUsefulOne()
            val two = doSomethingUsefulTwo()
            println("The answer is ${one + two}")
        }
        println("Completed in $time ms")
    }
    
    // 출력결과
    // The answer is 42
    // Completed in 2010 ms

    모든 프로세스가 종료되는 시간이 2.01초이다. (성능에 따라 상이) 각 함수에서 1초씩 연기시킨 후 리턴값을 연산 후 종료가 되었다.

     

    하나의 의문점 코루틴 내부에서도 비동기로 동작할 수 있게는 안될까?

     

    - Concurrent using async

    async는 launch와 같으며 코루틴 내부에 가벼운 스레드를 돌릴 때 사용한다.

    이때 반환되는 타입은 Deferred 타입이다. 코틀린 특성상 타입을 확인하긴 어렵지만 IDE(intelliJ)에서 친절하게 표시해준다.

    리턴된 반환 값에서 await() 메서드로 값을 가져올 수 있다.

    그리고 Defferred 타입은 Job을 상속받고 있어 컨트롤이 가능하다.

    // 출력결과
    // The answer is 42
    // Completed in 1015 ms

    위 async를 사용하면 결과값의 차이가 확연하게 다르다. 코루틴 내부에서도 스레드 작업이 진행된 것을 알 수 있다.

     

    - Lazily started async

    async함수에 CoroutineStart.LAZY 파라미터를 추가할 경우 선택적으로 async를 지연시킬 수 있다.

    fun main() = runBlocking {
        val time = measureTimeMillis {
            val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
            val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    
            one.start()
            two.start()
            println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
    }

    지연 설정 시 start() 메서드를 통해 실행시키거나 await() 메서드가 호출되면 자동실행된다. lazy는 kotlin에서 호출 전까지 메모리에 올라가지 않아 메모리 관리에 효과적이다.

    * start() 메서드를 호출 시 개인 코루틴이 동작하지만, await() 메서드 호출시 실행 및 대기상태로 진행되어 async의 기능을 발휘하지 못한다.(async 안 쓴 것과 동일한 효과)

     

    - Async-style functions

    위와 같이 코드를 간결하게 만들기 위해 async를 함수로 지정하여 사용할 수 있다.

    fun somethingUsefulOneAsync() = GlobalScope.async {
        doSomethingUsefulOne()
    }
    
    fun somethingUsefulTwoAsync() = GlobalScope.async {
        doSomethingUsefulTwo()
    }

    (함수 재사용 시 혼돈을 줄이기 위해 "... Async"를 붙이자) 함수는 하나의 호출 용도로 사용되어 별도의 suspend를 붙이않아도 된다.

     

    fun main() {
        val time = measureTimeMillis {
            val one = somethingUsefulOneAsync()
            val two = somethingUsefulTwoAsync()
    
            runBlocking {
                println("The answer is ${one.await() + two.await()}")
            }
        }
        println("Completed in $time ms")
    }

    (비추하는 방식)

    비동기를 사용하는 다른 언어에서 보편적으로 사용되는 스타일이다. 하지만 이런 형태로 만들 수 있다 정도만 알고 있다.

    다수의 async, 즉 다수의 코루틴이 실행되었고 하나의 코루틴이 예외가 발생했을 때 여전히 백그라운드에 실행 중으로 나탈 수 있다. 그래서 안드로이드에서는 ANR 이슈 발생의 가능성이 높다.

    아래와 같이 만들어 동시성을 부여하여 예외 발생될 경우의 이슈를 해결하자.

    suspend fun concurrentSum(): Int = coroutineScope {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
    
        one.await() + two.await()
    }
    
    fun main() = runBlocking {
        val time = measureTimeMillis {
            println("The answer is ${concurrentSum()}")
        }
        println("Completed in $time ms")
    }

    이 방법은 예외가 발생했을 때 해당 범위의 모든 코루틴이 취소되어 보다 안정적으로 사용할 수 있다.

     

    suspend fun failConcurrentSum(): Int = coroutineScope {
        val one = async<Int> {
            try {
                delay(Long.MAX_VALUE)
                42
            } finally {
                println("First child was cancelled")
            }
        }
        val two = async<Int> {
            println("Second child throws an exception")
            throw ArithmeticException()
        }
        one.await() + two.await()
    }
    
    fun main() = runBlocking {
        try {
            println("The answer is ${failConcurrentSum()}")
        } catch (e: ArithmeticException) {
            println("Computation failed with ArithmeticException")
        }
    }
    
    // 출력결과
    // Second child throws an exception
    // First child was cancelled
    // Computation failed with ArithmeticException

     

    Corutiune Context and Dispatchers

    코루틴은 항상 코틀린 표준 라이브러리에 정의된 CoroutineContext 타입의 값으로 표시되는 일부 컨텍스트에서 실행된다.(?)

    코루틴 컨텍스트는 다양한 집합요소로 이전의 Job과 아래에서 다룰 Dispatcher이다.

     

    - Dispatchers

    코루틴 컨텍스트는 코루틴을 실행하는 스레드거나 스레드를 결정하는 코루틴 디스패처를 포함한다.

     

    launch 및 async와 같은 모든 코루틴 빌더는 코루틴 컨텍스트를 파라미터로 받을 수 있다.

    fun main() = runBlocking<Unit> {
        launch {
            println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(Dispatchers.Unconfined) {
            println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(Dispatchers.Default) {
            println("Default               : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(newSingleThreadContext("MyOwnThread")) {
            println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
        }
    }
    
    // 출력결과
    // Unconfined            : I'm working in thread main
    // Default               : I'm working in thread DefaultDispatcher-worker-1
    // newSingleThreadContext: I'm working in thread MyOwnThread
    // main runBlocking      : I'm working in thread main

    • launch { ... }는 별도의 파라미터를 받지 않을 경우 시작되는 CoroutineScope에서 컨텍스트를(디스패처)를 상속합니다. 위의 경우 runBlocking의 메인 컨텍스트를 상속합니다. launch { ... } == launch(this.coroutineContext) { ... }
    • Dispatchers.Unconfined - main 스레드에서 동작하는 특별한 디스패처인 듯 하지만 실제로 다른 메커니즘을 가진다. 추후 설명
    • Dispatchers.Default - 코루틴이 GlobalScope에서 시작될 때 사용되는 기본 디스패처이며, 공유 백그라운드 스레드 풀을 사용한다. launch(Dispatchers.Default) { ... } == GlobalScope.launch { ...
    • newSingleThreadContext - 많은 비용의 리소스를 사용한다. 오랫동안 사용하지 않을 때는 및 릴리즈하거나 최상위 변수에 저장하여 어플 전체에서 재사용해야 한다.

    자동완성에는 하나 더 나와있다.(android 동일)

    Dispatchers.IO - 파일 입출력 스레드 풀 오프로딩하기 위해 설계된 디스패처이다. 디스패처 사용 스레드 수는 kotlinx.coroutines.io.parallelism(IO_PARALLELISM_PROPERTY_NAME) 시스템 특성 값으로 제한, 기본값은 64개의 스레드 또는 코어 수 (둘 중 큰 것)으로 제한된다. 이 디스패처의 경우 다른 스레드로 전환되지 않는다.

     

    -  Unconfined(제한되지 않은) vs confined(제한된) dispatcher

    Unconfined 디스패처는 스레드를 호출하여 시작하지만 첫 번째 정지 지점까지이다. 그 후 호출된 suspending 함수에 의해 완전히 결정된 스레드에서 코루틴을 재개합니다.(해당 범위에서 조작 불가능?) 그래서 cpu소비, 특정 스레드에서 사용되는(UI 업데이트)에는 적합하지 않다.

     

    confined 디스패처는 CoroutineScope의 상속받으며, 특히 runBlocking은 스레드가 제한돼 예측 가능한(FIFO) 스케줄링을 진행하고 스레드를 조작할 수 있다.

    fun main() = runBlocking<Unit> {
        launch(Dispatchers.Unconfined) {
            println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
            delay(500)
            println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
        }
    
        launch {
            println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
            delay(1000)
            println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
        }
    }
    
    // 출력결과
    // Unconfined      : I'm working in thread main
    // main runBlocking: I'm working in thread main
    // Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
    // main runBlocking: After delay in thread main

    Unconfined의 내용은 위 내용으로는 정확히 이해가 가지 않는다.

    실제 출력 결과를 보면 스레드 name 달라진 것을 확인할 수 있고 컨텍스트가 상속된 코루틴 runBlocking { ... }은 main 스레드에서 계속 실행되는 반면 delay후 defaultexecutor에서 실행된다.

    결론 코루틴을 디스패치 할 필요가 없거나 문제가 발생될 특이 사항에 대해 도움이 될 수 있겠지만, 일반적인 코드에서 사용하지 말자.

     

     

    끝.

    댓글

Designed by Tistory.