ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin Coroutines
    🔖 Kotlin 2024. 12. 28. 00:44

    🤔 Kotlin Coroutines

    Coroutine 이란?

    • co(함께) + routine (특정 일을 수행하는 명령의 모음)
    • 코틀린에서 제공하는 기능으로 비동기 작업을 효율적으로 처리하기 위해 설계된 경량화된 동시성 처리 방식

    📍 특징

    • suspend(중단)와 resume(재개)가 가능
    • 비 선점형 멀티태스킹 (Non-preemptive multitasking)
    • 협력형 멀티태스킹 (Cooperative multitasking)

     

    ❗ 왜 코루틴을 사용해야 할까요?

    📖 비동기 처리

    • 네트워크 요청, DB 작업, 파일 I/O 처럼 오랜 시간이 걸리는 작업을 비동기로 처리하는 것이 중요

    코루틴은 Kotlin 에서 동시성을 간단하게 처리할 수 있는 기능 중 하나.

    • 비동기 코드를 동기적으로 작성하는 것처럼 보이게해 코드의 가독성을 높이고 복잡한 콜백이나 다른 동시성 패턴에 비해 쉽게 작성할 수 있게 한다.
    • 코루틴은 중단할 수 있으며, 중단될 때 스레드를 블로킹하지 않고 다른 작업에 양보한다. 따라서 스레드를 다른 작업을 수행시키는데 사용할 수 있다.

     

    🆚 Subroutine

    Subroutine Coroutine
    Routine : 특정한 일을 처리하기 위한 명령 일시 중단과 재개가 가능한 작업 단위
     - 특정 지점에서 작업을 멈추고, 이후 필요할 때 재개 
    Subroutine : Routine의 하위에서 실행되는 또 다른 루틴 중단된 곳에서 다시 시작가능
    - 여러개의 진입점을 가진다
    서브 루틴은 하나의 진입점을 가지며, 한 번 호출되면 작업을 완료할 때까지 멈추지 않고 쭉 실행되어 스레드가 다른 작업을 할 수 없다 작업을 하지 않을 때는 스레드 사용 권한을 양보하며 서로 협력적으로 실행

    🏃 동작 방식

    Q. 코루틴은 어떻게 중단과 재개를 할까?

    ⇒ CPS (continuation Passing Style)

     

    CPS란 호출되는 함수에 Continuation을 전달하고 각 함수의 작업이 완료되는 대로 전달받은 Continuation을 호출하는 패러다임이다.

     

    @SinceKotlin("1.3")
    public interface Continuation<in T> {
        public val context: CoroutineContext
        public fun resumeWith(result: Result<T>)
    }
    • Continuation : 각 중단 시점마다 코루틴의 현재 상태(실행 지점, 로컬 변수 등)와 다음에 무슨 일을 해야 할지를 기억하는 확장된 콜백 역할을 하는 객체

     

    Continuation 인터페이스에는 크게 context 객체와 resumeWith 함수가 있다.

     

    • resumeWith : 특정 함수 a가 suspend 되어야 할 때, 현재 함수에서 a의 결과를 T로 받게 해주는 함수
      • 실행을 재개하기 위한 메서드
    • context: 각 Continuation이 특정 스레드 혹은 스레드 풀에서 실행되는 것을 허용해준다.
      • dispatcher, job 등 ConroutineContext를 저장하는 변수

     

    📍 정리

    CPS에서 Continuation을 전달하면서 현재 suspend된 부분에서의 resume을 가능하게 해준다.

    • Continuation은 resume 되었을 때의 동작 관리를 위한 객체

     

    Q. Continuation 객체는 어떻게 생기나?

     

    ⇒ 컴파일러가 suspend function을 만나게 되면 내부적으로 Continuation을 활용하도록 변환

     

    🏃 컴파일러의 suspend function 변환 과정

    // 예제
    suspend fun test() {
    		println("Hello")
    		delay(1000L) // 코루틴에서 제공하는 일시 중단 함수
    		println("World!")
    }
    suspend fun delay(timeMillis: Long)

     

     

    1. 컴파일러가 함수 내부의 중단 가능 지점 을 식별

    • 위 코드에서의 중단 가능 지점
      • suspend fun test()
      • delay(1000L)
      • suspend fun delay()

     

    2. 유한 상태 머신(Finite State Machine, FSM) 으로 변환

    suspend fun test() {
      when (label) {
        0 -> {
          println("Hello")
          delay(1000L)
        }
        1 -> {
          println("World!")
        }
      }
    }
    • 코틀린은 모든 중단 가능 지점을 찾아 when으로 표현
    • 중단 가능 지점을 기준으로 코드 블록을 분리하고 분리된 코드들은 label로 구별
    • label은 0부터 증가하고 이는 재개했을 때 돌아올 위치를 나타낸다.

     

    3. Continuation 추가

    fun test(continuation: Continuation) {
      val continuation = object : ContinuationImpl { ... }
    	
      when (continuation.label) {
        0 -> {
          println("Hello")
          delay(1000L)
        }
        1 -> {
          println("World!")
        }
      }
    }
    
    • suspend 키워드가 사라지고 파라미터로 continuation이 추가
    • continuation 인터페이스의 구현체인 Continuation object가 생성

     

    4. 실행 후 중단(Suspend)

    fun test(continuation: Continuation) {
      val continuation = object : ContinuationImpl { ... }
    	
      when (continuation.label) {
        0 -> {
          println("Hello")
          continuation.label = 1 // 중단 이후 라벨 1로 재개
    			
          if (delay(1000, continuation) == COROUTINE_SUSPEND) {
            return COROUTINE_SUSPEND
          }
        }
        1 -> {
          println("World!")
          return Unit
        }
      }
    }
    • label 값으로 실행 후 다음 값으로 값을 바꾼다.
    • suspend function은 COROUTINE_SUSPEND를 반환할 수 있다.
      • 이는 현재 코루틴이 중단 상태임을 나타낸다.
      • 이를 리턴해줌으로써 함수가 끝난 것 같은 효과를 주어 스레드를 블로킹하지 않을 수 있다

     

    5. 재개 (Resume)

    fun test(continuation: Continuation) {
      val continuation = object : ContinuationImpl { 
        override fun resumeWith() {
          test(continuation = this)
        }
      }
    	
      when (continuation.label) {
        0 -> {
          println("Hello")
          continuation.label = 1 // 중단 이후 라벨 1로 재개
          delay(1000L)
    		}
        1 -> {
          println("World!")
          return Unit
        }
      }
    }
    
    • Continuation의 resumeWith() 함수를 호출해서 중단된 이후 블록부터 코루틴을 재개

     

    🌟 성능 - Thread vs Coroutine

    Coroutine은 light-weight thread 라고 불린다.

    • Coroutine은 스레드 안에서 실행된다.
    • 같은 스레드에 10, 100개의 Coroutine이 있을 수 있다.
    • 하지만, 한 시점에 여러 Coroutine이 실행된다는 것은 아니다.
    • Coroutine은 메인 스레드 뿐 아니라 다른 백그라운드 스레드에서도 동작할 수 있다.

     

    ❗ 동시성 프로그래밍에서 한 시점에서는 하나의 Coroutine만 실행된다.

     

    즉, Coroutine은 스레드 안에서 실행되고 Coroutine을 활용하여 Main-safe 하게 할 수 있다.

     

     

    ✋ 스레드는 프로세스에 종속이 되지만, Coroutine은 특정 스레드에 종속되지 않는다는 점이 큰 특징이다.

    • Coroutine은 resume 될 때마다 다른 스레드에서 실행될 수 있다.

     

    💡 작업이 스레드에 종속되지 않는다?

    • 스레드를 blocking 하지 않으면서 작업의 실행을 잠시 중단할 수 있게 한다.
    • 스레드 B가 스레드 A의 작업이 끝날 때 까지 대기해야 하는 작업을 한다면 이를 잠시 중단하고 그동안 다른 작업을 할 수 있게 한다.
    • 이 특징은 스레드를 blocking 하지 않아 더 빠른 연산이 가능하게 하고 메모리 사용량을 줄여 많은 동시성 작업을 수행할 수 있게 한다.

     

    🧑‍💻 성능 차이 코드 예제

    fun main() = runBlocking {
    	val amount = 10_000 // 실행할 작업 수
    	println("✅ Comparing performance with $amount tasks")
    	println("🛫 ${Thread.activeCount()} threads active at the start")
    
    	// 코루틴 실행 시간 측정
    	val coroutineTime = measureTimeMillis {
    		createCoroutines(amount)
    	}
    	println("⏰ Running time for createCoroutines is $coroutineTime ms")
    
    	// 스레드 실행 시간 측정
    	val threadTime = measureTimeMillis {
    		createThreads(amount)
    	}
    	println("⏰ Running time for createThreads is $threadTime ms")
    }
    
    suspend fun createCoroutines(amount: Int) = coroutineScope {
    	val jobs = ArrayList<Job>()
    
    	for (i in 1..amount) {
    		jobs += launch(Dispatchers.Default) {
    			delay(1000L)
    		}
    	}
    	println("🛬 ${Thread.activeCount()} threads active after createCoroutines")
    	jobs.forEach { it.join() }
    }
    
    fun createThreads(amount: Int) {
    	val jobs = ArrayList<Thread>()
    
    	for (i in 1..amount) {
    		jobs += Thread {
    			Thread.sleep(1000L)
    		}.also { it.start() }
    	}
    	println("🛬 ${Thread.activeCount()} threads active after createThreads")
    	jobs.forEach { it.join() }
    }
    

     

     

    1. amount = 100

     

    2. amount = 1000

     

    3. amount = 10000

     

     

    📕 생성된 스레드 수

      Coroutines Threads
    amount = 100 13 thread 113 thread
    amount = 1000 13 thread 1013 thread
    amount = 10000 13 thread Out Of Memory

     

     

    📕 소요된 실행 시간

      Coroutines Threads
    amount = 100 1012 ms 1017 ms
    amount = 1000 1054 ms 1059 ms
    amount = 10000 1098 ms Out Of Memory

     

     

    ❗ 메모리 측면에서 큰 차이가 보인다.

    • Coroutine을 사용하면 불 필요한 blocking을 하지 않아 스레드 생성을 줄일 수 있기 때문

    📍 참고 자료

    Coroutines | Kotlin

    코루틴(Coroutine)에 대하여

    바삭한 신입들의 동시성 이야기 - Kotlin 편

    [10분 테코톡] 빙티의 코틀린 코루틴의 동작 방식

    코루틴(Coroutine)에 대하여

     

    '🔖 Kotlin' 카테고리의 다른 글

    😎 Infix Function  (0) 2024.12.30
    널 안정성  (0) 2024.12.29
    🦥 lateinit 과 lazy  (0) 2024.12.29
    Unit / Nothing  (0) 2024.11.25
    Gradle Kotlin DSL  (2) 2024.11.21

    댓글

Designed by Tistory.