관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 6. 코루틴 빌더 본문

책/코틀린 코루틴

[코틀린 코루틴] 6. 코루틴 빌더

참깨빵위에참깨빵 2024. 2. 16. 20:28
728x90
반응형

중단 함수는 Continuation 객체를 다른 중단 함수로 보내야 하기 때문에 아래가 성립한다.

 

  • 중단 함수는 일반 함수를 호출할 수 있다
  • 일반 함수는 중단 함수를 호출할 수 없다

 

때문에 모든 중단 함수는 다른 중단 함수에 의해 호출돼야 한다. 그렇다고 일반 함수에서 아예 중단 함수를 호출할 수 없는 건 아니다.

코루틴 빌더를 통해 일반 함수 안에서 중단 함수를 호출할 수 있다. 코루틴 빌더는 3종류 있고 서로 쓰임새가 다르다.

 

  • launch
  • runBlocking
  • async

 

launch

 

launch의 작동 방식은 thread 함수를 호출해 새로운 쓰레드를 시작하는 것과 같다.

 

import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("hello, ")
    Thread.sleep(2000L)
}

// hello,
// (1초 후)
// World!
// World!
// World!

 

launch는 CoroutineScope 인터페이스의 확장 함수로, 이 인터페이스는 부모와 자식 코루틴 사이의 관계 정립을 위해 쓰이는 구조화된 동시성(structured concurrency)의 핵심이다. 추가로 지금은 GlobalScope를 사용하지만 실제로는 사용하면 안 된다.

이유는 크게 아래와 같으며 더 자세한 내용은 별도의 포스팅으로 작성한다.

 

  1. GlobalScope로 생성한 코루틴은 cancel()을 통한 취소가 불가능하다
  2. GlobalScope는 launch, async와 달리 object로 구현됐기 때문에 메모리에 한 번 적재되면 클래스 소멸, JVM 중지, 프로세스 정지 중 하나가 발생하기 전까지 사라지지 않는다
  3. GlobalScope를 사용하면 그 안에서 여러 코루틴을 더 작은 스코프에 바인딩할 수 없어 구조화된 동시성의 이점을 누릴 수 없다

 

launch의 작동 방식은 데몬 쓰레드와 꽤 비슷하지만 훨씬 가볍다. 그래서 블로킹된 쓰레드를 유지하는 건 비용이 많이 들지만, 중단된 코루틴을 유지하는 건 거의 공짜에 가까운 비용이 소모된다.

 

runBlocking

 

코루틴은 쓰레드를 블로킹하지 않고 작업만 중단시킨다는 게 일반적이다. 하지만 블로킹이 필요할 수도 있다. 예를 들어 메인 함수에서 코루틴이 완료되는 것보다 프로그램을 빨리 종료시키지 않아야 할 때다. 이 때 runBlocking을 쓸 수 있다.

이 빌더로 생성한 코루틴이 중단되면 시작한 쓰레드를 중단시킨다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html

 

runBlocking

Runs a new coroutine and blocks the current thread until its completion. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests. Calling runBlocking from a suspend functio

kotlinlang.org

새 코루틴을 실행하고 완료될 때까지 현재 쓰레드를 중단한다. 일반 차단(regular blocking) 코드를 일시 중단(suspending) 스타일로 작성되 라이브러리에 연결해서 주요 함수 및 테스트에서 사용하도록 설계됐다. 일시 중단 함수에서 이것을 호출하는 것은 중복되므로 올바르지 않다...(중략)...실행되는 쓰레드가 해제(releasing)되는 대신 차단되어 쓰레드 고갈(thread starvation) 문제가 발생할 수 있다

 

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        delay(1000L)
        println("World!")
    }
    runBlocking {
        delay(1000L)
        println("World!")
    }
    runBlocking {
        delay(1000L)
        println("World!")
    }
    println("hello, ")
}

// (1초 후)
// World!
// (1초 후)
// World!
// (1초 후)
// World!
// hello,

 

공식 문서에서도 설명하듯 runBlocking이 쓰이는 경우는 2가지 정도다.

 

  • 프로그램 종료를 막기 위해 쓰레드를 블로킹해야 하는 메인 함수
  • 프로그램 종료를 막기 위해 쓰레드를 블로킹해야 하는 단위 테스트

 

async

 

launch와 비슷하지만 이 빌더는 값을 생성해서 리턴하도록 설계됐다. 리턴값은 람다식에 의해 리턴된다.

이 리턴값은 Deferred<T> 타입의 객체인데, Deferred에는 작업이 끝나면 값을 리턴하는 중단 함수인 await가 있다.

 

import kotlinx.coroutines.*

fun main() = runBlocking {
    val resultDeferred: Deferred<Int> = GlobalScope.async { 
        delay(1000L)
        42
    }
    
    // 다른 작업
    val result: Int = resultDeferred.await() // 1초 뒤에 result에 값이 대입됨
    println(result) // 42
    
    // 간단하게 작성할 경우
    println(resultDeferred.await())
}

 

또한 async는 launch와 비슷하게 호출되자마자 코루틴을 즉시 실행한다. 이 특징 때문에 몇 개의 작업을 한 번에 시작하고 모든 결과를 한꺼번에 기다리기 위해 사용한다.

리턴된 Deferred는 값이 생성되면 해당 값을 내부에 저장하기 때문에 await에서 값이 반환되는 즉시 사용할 수 있다.

 

이런 특징 때문에 보통 리턴값이 필요하다면 async를 통해 코루틴을 다루고, 리턴값이 필요없다면 launch를 통해 코루틴을 다뤄야 한다.

 

구조화된 동시성

 

앞서 말했지만 코루틴이 처음에 본 GlobalScope에서 시작됐다면 프로그램은 이 코루틴을 기다리지 않는다.

코루틴은 어떤 쓰레드도 블록하지 않기 때문에 프로그램 종료를 막을 수 없기 때문이다. 아래 예시에서 "World!"가 출력되려면 runBlocking 끝에 delay()를 사용해야 한다.

 

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.async {
        delay(1000L)
        "Text 1"
    }
    GlobalScope.async {
        delay(3000L)
        "Text 2"
    }
    GlobalScope.async {
        delay(2000L)
        "Text 3"
    }
    println("Hello, ")
//    delay(3000L)
}

// Hello,

 

처음에 GlobalScope가 필요한 이유는 launch, async가 CoroutineScope의 확장 함수기 때문이다. 두 빌더 함수와 runBlocking의 시그니처를 보면 block 파라미터가 리시버 타입이 CoroutineScope인 함수 타입인 걸 알 수 있다.

 

public actual fun <T> runBlocking(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

 

이 말은 GlobalScope 없이 runBlocking이 제공하는 리시버를 써서 this.launch 또는 launch를 써서 호출해도 된다는 뜻이다.

이렇게 하면 launch는 runBlocking의 자식이 된다. 부모 코루틴은 자식 코루틴들을 기다리기 때문에 runBlocking은 모든 launch들이 작업을 끝낼 때까지 중단된다.

 

import kotlinx.coroutines.*

fun main() = runBlocking {
    this.launch { // launch만 쓴 것과 같음
        delay(1000L)
        println("World!")
    }
    launch {
        delay(2000L)
        println("World!")
    }
    println("Hello, ")
}

// Hello,
// (1초 후)
// World!
// (1초 후)
// World!

 

부모 코루틴은 자식 코루틴들을 위한 scope를 제공하고 자식들을 그 scope 안에서 호출한다. 이를 통해 구조화된 동시성 관계가 성립한다.

부모-자식 관계에서 중요한 특징들은 아래와 같다.

 

  • 자식 코루틴(이하 자식)은 부모 코루틴(이하 부모)으로부터 컨텍스트를 상속받는다. 자식이 이를 재정의할 수도 있다
  • 부모는 모든 자식이 작업을 끝내기를 기다린다
  • 부모 코루틴이 취소되면 자식 코루틴들도 취소된다
  • 자식 코루틴에서 에러가 발생하면 부모 코루틴도 에러가 발생해 소멸한다

 

runBlocking은 CoroutineScope의 확장 함수가 아니다. 그래서 runBlocking은 자식 코루틴이 될 수 없고 루트 코루틴으로만 쓰일 수 있다.

이러한 것들 때문에 runBlocking은 다른 코루틴과 쓰임새가 다르고 다른 코루틴 빌더들과 차이가 존재한다.

 

coroutineScope 사용하기

 

repository의 함수에서 비동기적으로 2개의 자원, 예를 들어 유저 데이터와 글 목록을 가져온다고 가정한다. 그리고 유저가 볼 수 있는 글만 리턴한다고 가정한다.

async를 호출하려면 스코프가 필요하지만 함수 파라미터로 스코프를 넘기고 싶지 않을 수 있다. 중단 함수 밖에서 스코프를 만들려면 coroutineScope 함수를 사용한다.

 

suspend fun getArticlesForUser(
    userToken: String?
): List<ArticleJson> = coroutineScope { 
    val articles = async { articleRepository.getArticles() }
    val user = userService.getUser(userToken)
    articles.await()
        .filter { canSeeOnList(user, it) }
        .map { toArticleJson(it) }
}

 

coroutineScope는 람다식이 요구하는 스코프를 만드는 중단 함수다. 이 함수는 let, run, use, runBlocking처럼 람다식이 리턴하는 거라면 뭐든 리턴한다.

또한 coroutineScope는 중단 함수 안에서 스코프가 필요할 때 일반적으로 쓰이는 함수다. 중단 함수를 coroutineScope와 같이 쓸 수도 있는데 이것은 메인 함수와 runBlocking을 같이 쓰는 것보다 세련된 방법이다.

 

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(): Unit = coroutineScope {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello, ")
}

// Hello,
// (1초 후)
// World!

 

반응형
Comments