Coroutine이 뭘까?

hyihyi·2024년 8월 20일
post-thumbnail

이번에 Coroutine을 확실하게 정리해보자

📖 Coroutine이란?

스레드보다 가벼운 단위

여러 코루틴이 하나의 스레드에서 실행될 수도 있고, 필요에 따라 다른 스레드로 전환될 수도 있다.
코루틴은 일시 중단(suspend)되었다가 나중에 재개(resume)될 수도 있어, 비동기 작업을 보다 쉽게 관리할 수 있다.

🤔 스레드는 일시 중단과 재개가 안 되나?

될 수 있다. 하지만 스레드의 중단과 재개는 운영 체제에 의해 관리되며, 이는 스레드의 컨텍스트 스위칭으로 알려진 무거운 작업이다.

반면에 코루틴은 개발자가 작성한 코틀린 코드 내에서 직접적으로 관리되며, 운영체제가 아닌 코틀린 라이브러리와 런타임이 코루틴의 실행, 일시 중단, 재개를 처리한다. 코루틴은 메모리와 성능 오버헤드가 적어 수많은 코루틴을 하나의 스레드에서 효율적으로 실행할 수 있다.


📝 Coroutine 사용하기

1-1. Coroutine Builder

코루틴을 만드려면 Coroutine Builder를 사용해야 한다.
Coroutine Builder는 코루틴의 실행 범위와 동작을 정의

1) 실행 범위

CoroutineScope : 해당 스코프가 종료되면 그 안의 모든 코루틴도 취소됨

2) 스레드 설정

Dispatchers : 코루틴이 실행될 스레드를 지정

3) 종료 시점

빌더에 따라 종료되는 방식이 달라진다.

1-2. 주요 Coroutine Builder

1) launch

GlobalScope.launch {
    println("Task from launch")
}
println("Hello from main")
>> Hello from main
>> Task from launch

높은 확률로 위의 실행 결과처럼 "Hello from main"이 먼저 출력된다.

🤔 왜 코루틴이 먼저 실행되는데 먼저 출력되지 않는걸까?

비동기적으로 실행되는 작업은 실제로 실행되기 전에 스케줄링 과정을 거쳐야 한다.
launch로 시작된 코루틴은 비동기적으로 실행되므로 스케줄러에 의해 실행될 시점이 결정된 후에야 실제로 실행된다.

이 과정 때문에 메인 스레드에서의 코드가 코루틴보다 먼저 실행되는 것이다.

2) async

val deferred = GlobalScope.async {
    delay(1000L)
    "Result from async"
}
println("Async result: ${deferred.await()}")
>> Async result: Result from async

3) runBlocking

runBlocking {
    launch {
        delay(1000L)
        println("Task from runBlocking")
    }
    println("Hello from runBlocking")
}
>> Hello from runBlocking
>> Task from runBlocking

runBlocking은 동기적이고 블로킹 방식이다.
호출한 스레드를 차단해 runBlocking 블록 내의 모든 코드가 완료될 때까지 다른 작업이 실행되지 않도록 한다.

🤔 그럼 비동기 방식인 코루틴을 쓰는 의미가 없지 않을까?

맞다. runBlocking은 동기식으로 작동하며 호출한 스레드를 차단하기 때문에 비동기 작업을 수행하려는 목적에는 맞지 않을 수 있다.

🤔 그럼 언제 runBlocking을 쓸까?

1) 테스트 코드 : 비동기 작업의 결과를 동기적으로 기다려야 할 때
2) 메인 함수 : 간단한 스크립트에서 비동기 작업을 동기적으로 처리해야 할 때
3) 특수한 상황 : 동기식 처리가 필요한 경우

➡ 일반적인 상황에서는 runBlocking보다 비동기적인 코루틴 빌더들을 사용하는 것이 효율적이다.


2-1. CoroutineScope

코루틴의 실행 범위를 정의하고, 생명주기를 관리하는 역할을 함

Coroutine Builder와 CoroutineScope

  1. 코루틴 빌더는 항상 특정 CoroutineScope에서 호출된다.
  2. CoroutineScope는 코루틴의 생명주기를 관리하며, 스코프가 취소되면 해당 스코프 내의 모든 코루틴이 함께 취소된다.
  3. 특정 스코프 내에서 실행된 코루틴들이 스코프가 종료될 때 함께 종료되도록 보장

2-2. 주요 CoroutineScope

1) GlobalScope

애플리케이션 전체에서 사용할 수 있는 전역 스코프.

2) CoroutineScope(Custom Scope)

특정 클래스나 객체에 맞게 커스텀으로 정의한 스코프.

3) MainScope

UI 작업을 위한 메인 스레드에서 실행되는 스코프.

4) ViewModelScope

ViewModel의 생명주기에 따라 실행되는 스코프.

5) lifecycleScope

안드로이드의 LifecycleOwner(예: Activity나 Fragment)와 연계된 스코프


3-1. Dispatcher

코루틴이 실행될 스레드를 결정하는 역할

3-2. 주요 Dispatcher

1) Dispatcher.Main

UI 업데이트, 사용자 입력 처리 등 메인(UI) 스레드에서 수행해야 하는 작업에 사용

2) Dispatcher.IO

파일 읽기/쓰기, 네트워크 요청, 데이터베이스 접근 등을 처리하는 스레드 풀

3) Dispatcher.Default

복잡한 계산 작업이나 병렬 처리 등 CPU 집약적인 작업 스레드 풀

4) Dispatcher.Unconfined

특정 스레드에 구애받지 않으며, 처음에는 호출한 스레드에서 실행되지만, 첫 번째 중단점 이후 다른 스레드에서 실행될 수 있음. 일반적으로 잘 사용되지 않으며 테스트나 특수한 상황에서 주로 사용됨.

launch, async, runBlocking 모두 디스패처를 명시적으로 지정할 수 있다.

디스패처를 지정하지 않으면 기본적으로 CoroutineScope에 지정된 디스패처나 Dispatchers.Default를 사용한다.

val scope = CoroutineScope(Dispatchers.IO)

scope.launch(Dispatchers.Default) {
    // 이 코루틴은 Dispatchers.Default에서 실행됨
}

scope.async(Dispatchers.Main) {
    // 이 코루틴은 Dispatchers.Main에서 실행됨
}

이 경우, scope 자체는 Dispatchers.IO를 기본으로 사용하지만, 특정 코루틴에서 Dispatchers.Default나 Dispatchers.Main을 사용하도록 지정할 수 있다. 이렇게 하면 코루틴은 지정된 디스패처에서 실행된다.

profile
내가 이해하기 쉽게 쓰는 블로그

0개의 댓글