본문 바로가기
도서/코틀린 코루틴

Coroutine 완벽 정리

by 안스 인민군 2023. 2. 14.

면접 공부를 하면서 Coroutine을 정말 어설프게 써왔던 내 자신을 반성하며

다시한번 잘 정리해보도록 해보자.

Coroutine이란

쓰레드가 아니고 쓰레드와 비교하면 메모리를 덜쓰고오버핸드가 적은 비동기 프로그램을 실행 할 수 있는 모듈

코루틴은 AsyncTask라는 비동기 프로그래밍이 메모리 누수등 어려 문제가 생겨

API 30부터 deprecated 되었다.

이후, 구글에서는 코루틴을 원장하게 되었다.

먼저 코루틴을 알아 보기전에 루틴이란 개념에 대해 알아보도록 하자.

프로그램은 메인루틴, 서브 루틴 흐름의 루틴이 존재한다.

메인루틴은 프로그램에서 메인함수 main() 에 진입했을때 위에서 부터 아래로 순차적으로 실행되는 전체적인 흐름이고

서브 루틴은 메인 루틴이 실행되다 개별 함수를 만나면 잠시 함수로 들어가 순차적으로 실행되는 것을 말한다.

그렇지만 코루틴의 특징은 중간에 suspend 함수로 지연을 시켰다가 resume으로 다시 시작하여 작업을 끝나는 형태이다.

구글에서는 코루틴을 사용하라고 해서 메모리 누수를 막은 스레드라고 생각하기 쉬우나

스레드와 코루틴은 다르다.

  코루틴 스레드
메모리 구조의 차이 공유(프로세스의 힙 메모리를 사용함) 할당 (stack이라는 메모리를 할당받음)
메모리 구조에서의 관저에서 볼때는 코루틴은 함수에 가까움
수행 방식의 차이 비선점형 (동시성 O 병행성 X) 선점형(동시에 실행 = 병행성)
코루틴은 서로 전환 하면서 실행하는 속도가 매우매우 빠르기 때문에 동시에 실행하는것 처럼 보인다.
코루틴의 장점 메모리/오버헤드  
스레드를 사용하다보면 서로의 데이터를 참조해야 할 경우가 생기는데(Context Switching)이 줄어듦
코틀린에서 사용 suspend fun  

메모리 구조의 차이 예시
수행 방식의 차이

코루틴 구조

코틀린의 코루틴은 크게 Coroutine Scope, Coroutine Context, Coroutine Builder의 세 부분으로 나눌 수 있다.

Coroutine Scope

코루틴의 동작하는 범위를 규정합니다. 스코프 내에서 실행되는 코루틴의 실행을 감시하거나 취소할 수 있다.

코루틴 스코프는 두가지로 나뉘어지는데 CoroutineScope와 GlobalScope 두가지로 나뉘는데 

코루틴의 특징은 가볍게 쓰고 버리는 식으로 해야하는데

GlobalScope는 안드로이드에서 어플리케이션 라이프사이클을 따르며, 싱글톤으로 최상위 레벨에서 코루틴을 시작하기 때문에 필요할때 만들어 쓰고 버린다는 사용법이 불가능하다. 일반적으로는 사용을 추천하지 않는다.

CoroutineScope는 다음과 같은 인터페이스로 정의되며 특정한 dispatcher를 지정하여 동작이 실행될 스코프를 제한할 수 있다.

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Coroutine Context

코루틴은 항상 Coroutine Context 로 구성된 컨텍스트 안에서 실행 되는데, 이 컨텍스트는 Dispatcher과 job 두가지로 구성된다.

Dispatcher

Dispatcher는 코루틴이 실행될 스레드를 지정하는 역활을 하게 된다. 네가지 타입이 있는데 

1. Default : 주로 CPU에서 많은 연산이 필요한 처리

    - 공유 백그라운드 스레드의 common pool에서 동작

    - 동시작업이 가능한 최대 개수는 CPU코어수와 같으며 최소 2개

2. IO : 파일IO나 네트워크 콜을 수행해야 할 떄

    - 파일또는 소켓 IO등의 가볍고 빈번한 작업을 할때

3. Main: 코루틴에서 처리된 값을 UI에 반영할때,

    - 일반적으로 메인 스레드가 된다.

4. .Unconfined: 일반적인 용도에서는 사용 x

Job

코루틴의 추상적인 흐름을 job이라는 오브젝트로 만들어 취소나 예외처리를 용이하게 코루틴을 흐름제어한다.

val job = scope.launch {
    // New coroutine
}

코루틴은 일시정지될 수 있는 작업의 흐름이기 때문에 job은 코루틴의 여러가지 상태를 반영할 수 있도록 다음과 같이 설계되었다.

그리고 각 상태는 다음과 같이 전환 된다.

Job 객체에 대해서는 cencle, join, start 등의 메소드가 정의되어 있다. cancel과 start는 위의 다이어그램에서 표시되는 cancel과 start 동작을 구현하는 메소드이다. join은 코루틴을 병렬처리하지 않고 현재 job에 정의된 작업을 수행하는동안 기다리도록 하는 메소드이다

Coroutine Builder

- launch : 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과를 반환할 필요가 없는 작업에 사용하며 Job 객체를 반환한다.

- async : 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과를 반환할 필요가 있는 작업에 사용하며 Deferred 객체를 반환한다.

- runBlocking : 메인 스레드를 블록하고 작업을 실행한다. runBlocking은 테스트 용도등에나 사용하지, 코루틴을 위해서는 사용하지 말라고 권장하고 있다.

- withContext : 예를들어 Dispatchers.Main으로 지정된 스코프 안에서 Dispatchers.IO가 필요한 처리를 해야할 일이 있을 수 있다. 이 때 Dispatchers 안에 다시 Dispatchers를 정의할 수도 있지만 withContext를 사용하면 Dispatchers를 간편하게 스위치할 수 있다. withContext를 이용한 스코프 전환은 OS에서 관리되므로 오버헤드가 적다고 알려져 있다.

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

코루틴 지연

- delay : milisecond단위로 루틴을 잠시 대기시킨다. Thread.sleep은 스레드 자체를 정지시키는데 반해, delay는 코루틴이 멈추지 않고 대기상태에 들어간다는 점이 다르다.

- join : Job의 실행이 끝날때까지 대기시킨다.

- await : Deferred의 실행이 끝날때까지 대기시키고 결과값을 반환한다.

코루틴 예외처리

-CoroutineExceptionHandler를 이용하여 코루틴 내부의 기본 catch block으로 사용할 수 있다.

- launch, actor : exception발생 시 바로 예외가 발생.

- async, produce : 중간에 exception이 발생해도 await를 만나야 비로소 exception이 발생.

- Job.cancel()을 제외한 다른 exception이 발생하면 부모의 코루틴까지 모두 취소시킨다. 이는 structured concurrency를 유지하기 위함으로 CoroutineExceptionHandler를 설정해도 막을 수 없다.

- 자식 코루틴에서 exception이 발생하면 다른 자식 코루틴 및 부모코루틴이 다 취소되버리기 때문에, 문제가 생긴 코루틴만 exception 처리할 수 있도록 하기 위해 CoroutineExceptionHandler를 설정한다. 단, CancellationException는 handler에서 무시된다.

- 여러개의 exception이 발생하면 가장 먼저 발생한 exception이 handler로 전달되며 나머지는 무시된다.

 

 

정리

심플하게 정리하면 쓰레드가 아니고 쓰레드와 비교하면 메모리를 덜쓰고오버핸드가 적은 비동기 프로그램을 실행 할 수 있는 모듈이고 코루틴을 사용하기 위해서는 Coroutine Scope, Coroutine Context, Coroutine Builder을 사용해서 코루틴을 만들어서 사용하면 된다. 이때 그냥 스코프는 Coroutine Scope사용하면 되고 CPU작업이 많으면 Default, IO작업이 많으면 IO를 사용하면 된다. 그냥 코루틴작업을 실행하는 작업이라면 launch이고 반환할게 있다면 async를 통해 반환 한다.