[Kotlin] Coroutine

이제일·2023년 11월 12일
0

Kotlin

목록 보기
10/10

Coroutine

코루틴은 실행을 일시 중지할 수 있는, 스케쥴링 가능한 코드 블록 혹은 코드 블록들의 집합이다.

코루틴은 비동기 처리를 위한 기술로 스레드와 기능적으로 유사하지만,
스레드보다 가볍고 유연하며 여러 스레드 내에서 동일한 코루틴이 실행될 수 있다.

또한 코루틴은 Kotlin만 가지고 있는 기능이 아니다.
네이밍을 보면 "Ko"가 아닌 Co(함께) + Routine(Task) 이다.

위키피디아에서는 코루틴을 아래와 같이 말한다.

실행을 지연 및 재개함으로써 비선점적 멀티태스킹을 위한 서브 루틴을 일반화한 컴퓨터 프로그램 구성요소

메인 루틴과 서브루틴, 코루틴

  • 메인 루틴
    프로그램의 전체적인 동작으로 main 함수에 의해 수행되는 흐름
  • 서브 루틴
    반복적인 기능을 모은 동작으로 main 함수 내에서 실행되는 개별 함수의 흐름

간단하게 Main 함수와 Main에서 호출하는 함수로 생각하면 된다.

서브 루틴의 경우 동기적으로 실행되기에 진입점과 탈출점이 명확하지만
코루틴의 경우 진입 후 임의 지점에서 실행 중 동작을 중단하고 이후 해당 지점에서부터 실행을 재개한다.


내부적으로 Continuation dispatcher 등을 통해 Context 유지 및 적합한 쓰레드를 선택해 실행하게 된다.

코루틴은 왜 스레드보다 가볍나

1. 비선점형 멀티태스킹

  • 비선점형 (coroutine)
    하나의 프로세스가 CPU를 할당받으면 종료전까지 다른 프로세스가 CPU를 강제로 차지할 수 없다
    논리적인 병렬 실행으로 시분할로 cpu를 나눠 사용해 병렬 실행처럼 보이게 만듦

  • 선점형 (thread)
    하나의 프로세스가 다른 프로세스 대신에 CPU를 강제로 차지할 수 있다.
    물리적인 병렬 실행으로 실제로 동시에 작업을 진행

코루틴은 쓰레드가 아닌 쓰레드 내 동작하는 하나의 작업 단위이며 정의된 다양한 구성요소의 집합인 Context를 오버라이드 하며 실행된다.

따라서 쓰레드 내 Context switching 없이 여러 코루틴을 실행, 중단, 재개하는 상호작용을 통해 동시성을 갖기에 쓰레드와 메모리 사용이 줄어든다.

2. Dispatcher

코루틴은 Dispatcher에 의해 실행되는 환경(Thread)이 결정될 수 있지만, 그 자체로는 환경을 새로 구성하거나 변경하지 않는다.

만약 Dispatcher 를 재정의하지 않고 UI Dispatcher(Main Routine) 를 그대로 상속받아 사용한다면
일반적인 함수 호출(Sub Routine)과 동일하게 수행된다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(50_000) { // launch a lot of coroutines
        launch {
            delay(1000L)
            print(".")
        }
    }
}

따라서 위의 코드가 OOM(Out-Of-Memory) 없이 동작할 수 있다

실행 과정

  1. 속해있는 코루틴 스코프에서 코루틴 생성(왼쪽 그림)
    실행되는 함수들의 스레드는 Coroutine Scope가 갖는 Context의 디스페처에 의해 결정

  2. 생성시 Dispatcher를 설정할 경우 디스페처를 제외한 CoroutineContext를 전부 상속하여 생성

  3. launch { } 와 빌더를 통해 코루틴으로 실행할 코드블록은 Continuation 이라는 단위로 생성

  4. suspend(중단) 상태로 생성 되었다가 resume() 요청으로 인해 resumed 상태로 전환되어 내부 코드 로직 실행
    resume이 요청될 때마다 현재 컨텍스트의 dispatcher 를 통해 적합한 실행 스레드를 통해 로직 실행

함수들의 연결을 Continuation 이라는 Callback을 두어 연결하는 방식으로 Continuation 단위로 dispatcher 를 변경 또는 실행 유예 등 플로우 컨트롤 용이


Coroutine 구성 요소

suspend

suspend 키워드는 일시 중단 함수로 정의하며, CoroutineScope 내에서만 실행가능

suspend fun helloWorld() {
    delay(1000)
    println("hello World")
}

builder

현재 스레드에서 실행되는 코루틴을 만들고 코루틴이 완료될 때까지 현재 스레드의 실행을 Blocking 함

fun main(){
	runBlocking {
    	delay(100)
    	println("finish")
    }
}

CoroutineScope의 확장 함수로, 특정 코루틴 스코프에 코루틴 추가

fun main(){
	CoroutineScope(Dispatchers.Default).launch {
        delay(200)
        println("finish")
    }.join()
}

launch와 유사하지만 await을 통해 결과값 조회 가능


fun main(){
	val result = async { 
    	networkCall() 
    }
    println(result.await())
}
suspend fun networkCall(): String {
    delay(1000)
	return "Answer 1"
}

Dispatcher

  • Dispatcher.Default
    디스페처를 지정하지 않으면 사용되며,
    모든 표준 빌더에서 사용되는 기본 CoroutineDispatcher
fun main(){
	launch(Dispatchers.Default) {
    	// TODO
	}
}

  • Dispatcher.IO
    Blocking되는 IO 작업을 공유 스레드 풀로 offloading하도록 설계된 CoroutineDispatcher 입니다 .
fun main(){
	launch(Dispatchers.IO) {
    	// TODO
	}
}
  • Dispatcher.Main
    UI 객체로 작동하는 메인 스레드로 제한된 코루틴 디스패처
    일반적으로 단일 스레드로 구성
fun main(){
	launch(Dispatchers.Main) {
    	// TODO
	}
}


  • Dispatcher.Unconfined
    특정 스레드에 국한되지 않는 코루틴 디스패처
    특정 스레딩 정책을 요구하지 않고 해당 일시 중지 함수에서 사용하는 스레드에서 코루틴을 재개

아무 Context 를 지정하지 않고, 호출한 스레드에서 동작되게끔 한다

fun main(){
	launch(Dispatchers.Unconfined) {
    	// TODO
	}
}

Exception

코루틴 내부에서 오류가 발생했을 때 에러를 처리할 수 있는 CoroutineContext

fun main() = runBlocking {
     val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler : $exception")
    }
     
	CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
        throw IllegalArgumentException()
    }.join()
}

Coroutine Flow


지정된 suspend 블록 에서 cold flow 생성

cold flow

  • 연산자가 결과에 적용될 때마다 블록이 호출된다는 것을 의미
  • 단 한명의 구독자(collect 호출한 개체)만 존재하며 만약 새로운 구독자가 생길 경우 플로우를 다시 생성.
  • 수집(coolect)은 생산자 코드(flow를 업데이트하는 코드)를 트리거합니다.
  • collect 될 때마다 동작하지만, collect하는 소비자가 없으면 동작하지 않습니다.
fun fibonacci(): Flow<BigInteger> = flow {
    var x = BigInteger.ZERO
    var y = BigInteger.ONE
    while (true) {
        emit(x)
        x = y.also {
            y += x
        }
    }
}

fibonacci().take(100).collect { println(it) }

여러 개의 flow를 하나로 결합하는 연산

val flow = flowOf(1, 2, 3).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c", "d").onEach { delay(15) }
flow.zip(flow2) { i, s -> i.toString() + s }.collect {
    println(it) // Will print "1a 2b 3c"
}
profile
세상 제일 이제일

0개의 댓글