본문 바로가기

코틀린

코틀린 코루틴 개념 및 기본 사용 용도 정의

반응형

1. 개념

기본 개념은 하나의 스레드가 하나의 작업을 하는 것 과는 달리 하나의 스레드가 하나의 작업만을 사용하는게 아니라(delay등의 일시정지등을 통해) 유기적으로 하나 혹은 다른 작업들을 실행

ex) 네트워크 작업등을 처리하는데 있어서 스레드가 서버의 응답을 마냥 기다리기만 하는게 아니라 기다리는 동안 다른 작업을 처리함

 

2. Coroutine Context

 

CoroutineContext는 일련의 다음 요소를 사용하여 코루틴의 동작을 정의합니다. 

 

Job: 코루틴의 수명 주기를 제어한다.

CoroutineDispatcher: 적절한 스레드에 작업을 전달한다. (2.1 디스패처)

CoroutineName: 디버깅에 유용한 코루틴의 이름이다.

(CoroutineName을 설정하여 디버깅 등에 이용가능

ex)

someScope.lanuch(CoroutineName("hello wold!")) {

    println(Thread.currentThread().name)

})

 

CoroutineExceptionHandler: 포착되지 않은 예외를 처리한다.

ex)

val handler = CoroutineExceptionHandler { _, exception -> println(exception) }

someScope.launch(handelr) {

})

 

구성

@SinceKotlin("1.3")
public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

위의 key value 방식을 통해 context를 함께 사용할 수 있다.

plus메소드를 통해 Dispatchers.Main + Job() + CoroutineName("launch1") + handler 와 같이 사용 스레드, job, 이름 설정, execptionhandler 등을 함께 사용할 수 있다.

 

1) 디스패처

Dispatchers.Main - 이 디스패처를 사용하여 기본 Android 스레드에서 코루틴을 실행합니다. 이 디스패처는 UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 합니다. 예를 들어 suspend 함수를 호출하고 Android UI 프레임워크 작업을 실행하며 LiveData 객체를 업데이트합니다.
Dispatchers.IO - 이 디스패처는 기본 스레드 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있습니다. 예를 들어 회의실 구성요소를 사용하고 파일에서 읽거나 파일에 쓰며 네트워크 작업을 실행합니다. (Retorift에서는 이미 IO를 사용하고 있기 때문에 별도 처리가 필요 없다. 참고 https://thdev.tech/kotlin/2021/01/12/Retrofit-Coroutines/)
Dispatchers.Default - 이 디스패처는 CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화되어 있습니다. 예를 들어 목록을 정렬하고 JSON을 파싱합니다.

 

3.코루틴 빌더

launch 새 코루틴을 시작한다. 결과를 받지 않고 Coroutine Context를 이용하여 다양하게 사용가능

async awit을 통해 결과를 받아 처리하기가 편함 (물론 launch안에서 job의 join등을 이용해 비슷하게 처리 가능)

dealy 중지함수이다.  특징으로는 중지된 동안 다른 코루틴을 실행한다.(sleep은 스레드 자체를 중지하여 순차 실행이다.)

 

async ex)

suspend fun test1(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

suspend fun test2(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

fun main() {
    runBlocking {
        val totalTime = measureTimeMillis {
            val deferredOne = async { test1() }
            val deferredTwo = async { test2() }

            println("deferredOne = ${deferredOne.await()} deferredTwo = ${deferredTwo.await()}")
        }
        println(totalTime)
    }
}

코루틴 내에서 test1()과 test2()를 호출하였다면 2초이상이 걸렸겠지만 async나 launch등으로 새로운 코루틴 빌더내에서 호출하면 1초 조금 넘는 작업시간이 걸린걸 확인할 수 있다.

val deferreds = listOf(     
    async { test1() },  
    async { test2() }   
)
deferreds.awaitAll()

async를 각각 사용하지 않고 list 형태로 awaitAll()하는 방법도 있다.

출력)
deferredOne = 381 deferredTwo = 134
1035

코루틴 스코프 빌더

1)runBlocking 

runBlocking은 코루틴이 끝날때까지 기다린 후 이후 코드가 실행된다.

ex)

fun main() {
    runBlocking { // this: CoroutineScope
        launch { // launch a new coroutine and continue
            delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
            println("World!") // print after delay
        }
        println("Hello") // main coroutine continues while a previous one is delayed
    }
    println("end") // runblocking이 종료되고 나서 호출
}

출력)

Hello

World! //delay 되는 동안 다른 코루틴이 먼저 실행

end //runBlocking이 종료 되고나서 end가 출력된다.

참고)

코루틴 스코프 빌더 안에 실행되는 코루틴 스코프 빌더들은(withcontext등 포함) 및 suspend 함수는 내부 수행을 기다린다.(이게 완료될 때까지 runblocking 처럼 순차로 적용된다.)

내부 수행의 launch안에서 실행되는 부분은 순차 가아닌 비동기로 실행 된다.

ex)

someScope.launch {
    scope.launch {
        launch {} //비동기
        launch {} //비동기
    } //동기(이 작업이 끝나야 아래 부분 및 스코프 밖의 작업이 진행된다.)
    launch {} //비동기
}

2)CoroutineScope

CoroutineScope에서는  launch 또는 async를 사용 가능하다. 위에 정의된 context등을 통해 다양하게 조합하여 사용할 수 있다.

가장 대중적으로 선호되는 스코프지만 안드로이드에서는 라이프 사이클에 따라 이용 가능한 lifecyclescope, viewmodelscope가 우선으로 쓰인다.(둘다 CoroutineScope를 상속하여 만들어짐)

 

3)GlobalScope

GlobalScope역시  launch 또는 async 및 context등을 통해 다양하게 조합 가능하다.

하지만 singleton으로 되어 있어 특정한 경우(동일 스코프를 사용)등을 제외하고 서는 사용하는 것을 지양하는게 좋다.

 

참고
https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko

https://kotlinlang.org/docs/coroutines-basics.html

https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko 

https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine

 

반응형

'코틀린' 카테고리의 다른 글

비동기 callback을 동기로 처리하기 suspendCoroutine, suspendCancellableCoroutine  (0) 2022.05.23
코틀린 StateFlow 및 SharedFlow  (2) 2022.05.21
코루틴 채널 개념 및 예제  (0) 2022.05.07
JVM  (0) 2021.05.20
primitive wrapper  (0) 2021.05.19