개발/Android

코루틴 Coroutine

Debin 2022. 6. 1.
반응형

안드로이드에서 프로세스

  • 애플리케이션이 시작되면 안드로이드 시스템은 애플리케이션을 위한 새로운 프로세스와 스레드를 생성한다.
  • 기본적으로 애플리케이션 안의 모든 컴포넌트들은 동일한 프로세스의 동일한 스레드에서 실행한다.
  • 이 기본적인 스레드를 메인 스레드라고 하며, 메인 스레드는 위젯에게 이벤트를 전달하거나 화면을 그리는 등 UI 관련 모든 작업을 담당한다.

문제점

  • 애플리케이션에서 많은 작업을 해야 한다면 이와 같은 단일 스레드 모델은 애플리케이션의 성능을 저하시킨다.
  • 대기 시간이 길어지는 네트워크 요청 등의 기능을 수행할 때는 화면에 보이는 UI도 멈춤 상태인 ANR(Application Not Responding) 오류가 발생한다.
  • 해결방안으로는 하나의 프로세스 안에서 여러 개의 작업을 동시에 할 수 있는 멀티 스레드 방식을 채택하는 것이다.
  • 그러나 멀티 스레드도 역시 데드락이라는 문제점을 가지고 있다.

스레드

  • 화면 UI의 리소스 객체는 메인 스레드에서 관리하기 때문에 서브 스레드에서 접근할 수 없다.
  • 안드로이드에서 메인 스레드에서만 UI 작업이 가능한 이유는, 두 개 이상의 스레드가 동시에 화면에 접근하는 것을 막기 위함이다.

동기와 비동기

동기

  • 동기적이란 요청을 보낸 후 응답(결과)를 받아야 다음 동작을 수행하는 방식이다.
  • 프로그래밍에서 순서대로 작업을 수행하여 1개의 동작을 완료한 후 다른 동작을 실행하는 방식이다.
  • 다양한 기능이 한꺼번에 일어나는 다중 실행 환경에서는 성능상 제약이 발생한다.
  • 다중 실행 환경이란 UI 갱신, 네트워크 작업 등 다양한 작업을 동시에 대응해야 하는 실행 환경을 말한다.

비동기

  • 비동기란 요청을 보낸 후 응답(결과)과는 상관없이 다음 동작을 수행하는 방식이다.
  • 여러 개의 동작이 선행 작업의 순서나 완료 여부와 상관없이 함께 실행되는 방식이다.
  • 안드로이드에서는 AsyncTask, RxJava, Coroutine을 사용하여 비동기 방식을 처리한다.
  • 코틀린에서는 코루틴(Coroutine)을 사용하여 비동기 작업을 처리한다.

Coroutine 코루틴

Thread

  • 선점형 멀티태스킹
  • 작업 단위는 쓰레드로, 작업 각각에 쓰레드를 할당한다. 쓰레드는 자체 Stack 메모리 영역을 확보한다.
  • 쓰레드간 전환 시에 운영체제 거널에 의한 문맥 교환이 필요하므로 그만큼의 비용이 발생한다.
  • 멀티 쓰레드에서 병행성과 병렬성을 달성한다.
  • 쓰레드와 콜백을 많이 사용하면 코드가 복잡해진다. 

Coroutine

  • 비선점형 멀티태스킹
  • 작업 단위는 Coroutine object로, 작업 각각에 Coroutine object를 할당하고, 이때 Coroutine object는 Heap 영역에 적재된다.
  • 쓰레드위에서 동작하는 light-weight 쓰레드로 병행성(동시성)을 제공 (병렬성은 제공하지 않는다)하기 때문에 여러 작업이 동시성을 구현하면서도 스레드보다 비용이 적은 멀티태스킹 방식이다.
  • 코루틴은 문맥 교환없이 해당 루틴을 일시 중단을 통해 제어한다.
  • 코루틴은 비동기 코드를 마치 동기 코드처럼 쉽게 작성하면서도 비동기 효과를 낼 수 있다.
  • 하나의 개별적인 작업을 루틴이라고 하는데, 코루틴이란 여러 개의 루틴들이 협력한다는 의미의 합성어다.
  • 코루틴은 메인루틴과 별도로 코루틴을 생성하여 루틴의 실행과 종료를 제어할 수 있는 비동기 처리방식이다.

코루틴 동작 이미지

코루틴 구조

  • Coroutine Scope: 코루틴 동작 범위 지정
  • Coroutine Context: 코루틴 실행 스레드 지정
  • Coroutine Builder: 코루틴 실행 빌더 (작업 시작)

코루틴 구조

Coroutine Scope

코루틴이 동작하는 범위를 지정하는 것으로 CoroutineScope, GlobalScope로 구분한다.

  1. CoroutineScope
    • CoroutineContext를 가지고 있는 인터페이스로서 동작 범위(Scope)가 결정되면, 특정한 Dispatcher를 지정해서 스코프가 수행될 스레드를 지정한다.
    • 필요할 때 매번 생성하고, 사용을 마치면 종료한다.
  2. GlobalScope
    • 애플리케이션의 생명주기를 갖는 singleton object로서, 애플리케이션 레벨(Top-level)에서 코루틴을 실행한다.
    • 따라서 GlobalScope.launch() 로 실행한 코루틴은 애플리케이션이 종료되지 않는 한 필요한 만큼 실행한다.

Coroutine Context

  • CoroutineContext는 어떤 스레드에서 코루틴을 실행할 것인지 결정하는 역할을 맡는다.
  • 어떤 스레드에서 해당 코루틴을 실행할지에 대한 Dispatchers 정보를 담고 있다.
  • Dispatchers는 코루틴의 실행을 진행할 특정 스레드를 지정하거나, 스레드 pool을 지정한다.
    (코루틴에서 사용할 스케줄러를 정의 - Default(Work Thread), IO, Main(UI Thread) 등 )
  • 안드로이드에서 I/O 작업을 백그라운드 스레드에서 수행하고, 화면 갱신을 UI 스레드에서 처리하는 것처럼, 실행에 따라 사용할 스레드를 지정해야 하는데, 이때 CoroutineContext를 사용한다.

CoroutineBuilder

코루틴은 빌더를 사용하여 코루틴을 생성하고, 실행코드를 CoroutineScope 안에서 실행한다.

1.launch

  • 일반적인 코루틴을 시작할 때 사용하며, launch에서 사용할 스레드를 지정하지 않으면, CoroutineScope의 스케줄러를 따르며, 지정할 경우 지정된 스레드에서 동작한다.
  • 기본적으로 즉시 실행하며 블록 내의 실행 결과를 반환하지 않는다.
  • launch는 CoroutineScope를 실행하고 job 객체를 리턴한다.
  • job을 이용하여 코루틴 실행을 모니터링 및 제어할 수 있다.

2. async

  • 기본적으로 launch와 같은 동작을 하지만 실행 결과를 반환한다.
  • 실행 결과는 Deffered<T> 타입으로 객체를 반환하며, await() 함수를 통해 결과를 받는다.
  • await은 코루틴의 작업이 완료될 때까지 대기한다.

3.runBlocking

  • runBlocking은 scope 빌더로, runBlocking 블록을 실행하는 동안 main 스레드가 blocking 된다.

4. withContext

  • Dispatchers를 전환하는 기능을 수행하는 scope 빌더다.
  • 특정 스레드에서 코루틴을 수행하다, 필요에 따라 특정 스레드로 전환하여 어떤 작업을 수행해야 할 때 사용한다.
  • 예를 들어 IO 스레드에서 네트워킹 작업을 수행하다, 응답 결과를 UI에 갱신하기 위해 main 스레드로 전환한다.

Job

  • Job은 interface로 정의되어 있으며 현재 실행되고 있는 코루틴의 상태 값을 가지고 있는 인스턴스 객체다.
  • CoroutineScope launch{} 사용 시 job 객체를 반환한다.
  • Job을 통해 routine의 취소, 실행, 종료, 대기를 할 수 있다.
    • job.start(): coroutine의 동작 상태를 확인한다. (true: 동작 중, false: 준비 중/ 완료)
    • job.join(): coroutineScope의 동작이 종료할 때까지 대기
    • job.cancel(): coroutineScope의 동작 종료를 유도한다.
    • job.cancelAndJoin(): coroutineScope의 동작 종료를 요청하고 대기한다.
  • 자식 코루틴은 부모 코루틴의 job이 취소되면 함께 취소되며, 자식 코루틴만 취소하고 싶다면 별도의 job을 할당하여 취소한다.

job을 사용하는 방법은 2가지가 있다.

  1. 특정 CoroutineScope에서 launch의 반환 값을 사용하여 제어하는 방법이 있다.
  2. 전역에서 여러 개의 코루틴을 한 번에 제어하려면 독립적인 job을 생성하고, 이를 특정 CoroutineScope를 선언할 때, 
     CoroutineContext + job 형태로 선언한다.

job 사용하기
job status

코루틴 지연 메서드는 3가지가 있다.

  1. delay
  2. join
  3. await

코루틴 취소 메서드는 4가지가 있다.

  1. cancel
  2. cancelAndJoin
  3. withTimeout
  4. withTimeoutOrNull

예제 코드

우선 프로젝트에 코루틴 의존성을 추가해야한다. build.gradle에 다음과 같은 코드를 추가하자.

implementation 'org.jetbrains.kotlin:kotlinx-coroutines-core:1.6.0-native-mt'

먼저 CoroutineEx01.kt 코드다.

fun main(){
    println("Current Thread = ${Thread.currentThread().name}")

    GlobalScope.launch {
        println("Current Thread = ${Thread.currentThread().name}")
        delay(1000)
        println("World!!!")
    }

    println("Hello")
    Thread.sleep(2000)
}

출력 결과는 아래와 같다.

다음은 CoroutineEx02.kt 코드다.

fun main(){
    println("Current Thread = ${Thread.currentThread().name}")

    GlobalScope.launch {
        println("Current Thread = ${Thread.currentThread().name}")
        delay(1000)
        println("World!!!")
    }

    println("Hello")
    
    //새로운 코루틴을 생성하고, 실행이 완료될 때까지 현재 스레드를 블라킹한다.
    runBlocking {
        delay(1000)
    }
}

 

이상으로 포스팅을 마칩니다. 감사합니다.

 

참고자료

https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

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

 

반응형

댓글