[Android] Coroutine

mingsso·2023년 11월 14일

Android

목록 보기
7/12

1️⃣ 코루틴(Coroutine)이란?

비동기적으로 실행되는 코드를 간소화하기 위해 안드로이드에서 사용할 수 있는 동시 실행 설계 패턴
Kotlin 뿐만 아니라 Python, C#, JS 등 여러 언어에서 지원하는 개념임

코루틴을 사용하면 상대적으로 간단하고 가독성 높게 동시성 관리가 가능함

"코루틴이란 실행의 지연과 재개를 허용함으로써, 비선점적 멀티태스킹을 위한 서브루틴을 일반화한 컴퓨터 프로그램 구성요소이다" - 위키피디아 정의

  • 비선점형 멀티태스킹: 하나의 Task가 Scheduler로부터 CPU 사용권을 할당받았을 때, Scheduler가 강제로 CPU 사용권을 뺏는 것이 불가능함
  • 즉, 스레드는 실행되다가 OS에 의해 제어권이 뺏길 수 있으나(선점형), Coroutine은 중단 지점을 만나지 않는 한 제어권을 양도하지 않고 이어서 계속 실행함(비선점형)

위의 그림처럼 하나의 스레드 안에 여러 개의 코루틴이 존재할 수 있음
(스레드는 작업이 실행되는 곳이며, 코루틴이 하나의 작업인 것)

Coroutine과 기존 Routine과의 차이점

  • Main과 Sub의 개념이 존재하지 않으며, 모든 routine들이 서로를 호출할 수 있음
  • Sub routine처럼 리턴문이나 마지막 코드까지 도달하지 않더라도, 언제든지 중간에 나갈 수 있고 다시 나갔던 지점으로 들어올 수 있음
  • Sub routine은 진입점과 반환점이 단 1개이며, Main routine에 종속적이지만
    Coroutine은 진입점이 여러 개이며, Main routine에 종속적이지 않음
    -> 대등하게 데이터를 주고 받을 수 있음

  1. 어떤 함수 A가 실행되다가 Coroutine B를 호출함
  2. A가 실행되던 스레드 안에서 Coroutine B의 실행이 시작됨
  3. Coroutine B는 실행을 진행하다가, 실행을 A에 양보함
  4. A는 다시 Coroutine을 호출했던 바로 다음부터 실행을 계속 진행하다가, 또 Coroutine B를 호출함

-> 이때 B가 일반적인 함수였다면 로컬 변수를 초기화하면서 처음부터 실행을 다시 시작하겠지만, 코루틴이면 이전에 yield로 실행을 양보했던 지점부터 실행을 계속하게 되는 것

따라서 코루틴을 사용하는 경우, 일반적인 프로그램 로직을 기술하듯 코드를 작성하고
상대편 코루틴에 데이터를 넘겨야 하는 부분에서만 yield를 사용하면 됨!

Thread vs Coroutine

코루틴은 OS가 아닌 스레드 내에서 다른 코루틴에게 제어권을 넘겨주기 때문에,
스레드 내의 모든 코루틴은 OS에서 관리하는 다른 스레드에 제어권을 양도하지 않고 해당 스레드의 Time Frame을 계속 이용할 수 있음

즉, 코루틴은 OS가 아닌 사용자에 의해 시분할을 달성하게 됨

따라서 스레드를 통해 동시성 프로그래밍을 할 경우
CPU가 매번 스레드를 점유했다가 놓아주는 과정을 반복해야 하기 때문에 많은 비용이 들지만, 코루틴을 사용하면 더 적은 비용이 든다


또한 스레드는 CPU 사용과 시스템 오버헤드라는 관점으로 볼 때 유한한 리소스라는 점이 문제임
내부적으로 스레드 생성, 스케줄링, 파기를 위해 많은 작업이 진행됨

현대적인 CPU들은 수많은 스레드를 실행할 순 있지만, 특정 시점에 병렬적으로 실행될 수 있는 숫자는 CPU 코어 수로 제한됨 (보통 안드로이드 기기들의 CPU는 4 코어)

CPU 코어 숫자보다 많은 수의 스레드가 필요하면, 시스템은 스레드 스케줄링을 수행해 사용할 수 있는 코어들 사이에서 이 스레드들의 실행을 공유할 수 있는 정책을 결정함

따라서 실행하려는 작업이 시간이 얼마 걸리지 않거나, I/O에 의한 대기 시간이 크고, CPU 코어 수가 작아 동시에 실행할 수 있는 스레드 개수가 한정된 경우
코루틴으로 비동기 처리를 하면 좋음



2️⃣ 코루틴의 사용법

코루틴 디스패처 (Dispatchers)

Dispatch의 뜻은 '보내다'로, 디스패처는 스레드에 코루틴을 보내는 역할을 함

즉, 코루틴을 만든 다음 해당 코루틴을 디스패처에 전송하면 디스패처는 자신이 관리하는 스레드풀 내의 스레드 부하 상황에 맞춰 코루틴을 배분함

위의 그림에서 디스패처는 쉬고 있는 Thead2에게 Coroutine3을 배분하게 됨

안드로이드에는 이미 디스패처가 생성되어 있기 때문에, 별도로 생성하거나 정의할 필요가 없음

  • Dispatchers.Main: 메인 스레드에서 해당 코루틴을 실행함. 보통 UI를 변경하는 경우 사용함
  • Dispatchers.IO: 네트워크, 디스크, 데이터베이스 작업을 수행하는 코루틴에 적합함
  • Dispatchers.Default: 데이터 정렬, 복잡한 계산 수행과 같이 많은 CPU를 수행하는 경우 효과적임
coroutineScope.launch(Dispatcher.IO) {
	performSlowTask()
}



코루틴 스코프 (Coroutine Scope)

코루틴은 코루틴 스코프 안에서만 동작함
특히 일시중단 함수(await, join, delay, suspend)는 코루틴 스코프 안에서만 호출이 가능함

코루틴 스코프를 사용하기 위해서는, 미리 정의된 코루틴 스코프를 사용하거나 직접 생성하는 방법이 있음

  • GlobalScope
    • 애플리케이션이 시작하고 종료될 때까지 계속 유지됨
    • 싱글톤이기 때문에, 따로 생성하지 않아도 되며 어디서든 바로 접근이 가능함
    • But, 메모리 누수의 원인이 될 수 있기 때문에 신중히 사용해야 함
    • 즉, 앱이 실행된 이후 계속 수행되어야 한다면 Global Scope를 사용해야 하지만, 특정 액티비티/서비스에서만 잠깐 사용하는 경우에는 사용하지 않음
      fun main() {
          GlobalScope.launch {
              // run coroutine
          }
      }
  • viewModelScope
  • lifecycleScope
  • CoroutineScope
    • 코루틴 스코프를 직접 생성하여 사용하기
      val scope = CoroutineScope(Dispatchers.Main)

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



코루틴 빌더 (Coroutine Builder)

코루틴을 실행할 때 사용하는 여러가지 함수

  • launch
    • 현재 스레드를 중단하지 않고 새로운 코루틴을 생성함
    • 특정 결과값을 반환하지 않고, Job 객체를 반환함
      val job = CoroutineScope(Dispatchers.IO).launch() {
          delay(3000)
          println("launch 완료")
      }

  • async
    • 새로운 코루틴을 생성하고, await라는 정지 함수로 결과를 반환하도록 허용함
      CoroutineScope(Dispatchers.Default).launch() {
          val networkingDeferred = async(Dispatchers.IO) {
              delay(3000)
              "some networking.."
          }

          Log.d(TAG, "${networkingDeferred.await()}")
      }

  • withContext
    • 현재 실행되고 있는 코루틴의 컨텍스트를 변경할 때 사용함
    • 코루틴은 항상 컨텍스트 안에서 실행되며, 컨텍스트는 코루틴이 어떻게 실행되고 동작해야 하는지 정의할 수 있게 해주는 요소들의 그룹임 (디스패처, 예외 처리기, 코루틴 네임)
    • 코루틴 스코프는 코루틴 컨텍스트를 가짐
      GlobalScope.launch(IO) {
          val result = fetch(httpUrl)

          withContext(Main) {
              resultTextView.text = result.toString()
          }
      }

  • coroutineScope
  • supervisorScope
  • runBlocking
    • 한 코루틴을 실행하고 해당 코루틴이 완료될 때까지 현재 스레드를 중지시킴
    • 일반적으로 코루틴을 사용하고자 하는 의도에 반하기 때문에, 일반 비즈니스 로직에 사용되지 않으며 Unit test 등에 사용됨
      runBlocking {
          delay(5000)
          println("step 1")
      }
      println("step 2")






참고자료

도서 '핵심만 골라 배우는 젯팩 컴포즈'
https://hodie.tistory.com/72
https://kotlinworld.com/141
https://origogi.github.io/coroutine/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%8A%A4%EC%BD%94%ED%94%84/
https://jutole.tistory.com/15
https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko
https://taehyungk.github.io/posts/android-kotiln-coroutine-async-await/

profile
🐥👩‍💻💰

0개의 댓글