깨알 개념/Kotlin

[Kotlin] Coroutines - runBlocking

interfacer_han 2024. 2. 13. 17:52

#1 이전 글

 

[Kotlin] Coroutines - Coroutine builder

#1 Coroutine builder kotlinx-coroutines-core Core primitives to work with coroutines. Coroutine builder functions: Coroutine dispatchers implementing CoroutineDispatcher: More context elements: Synchronization primitives for coroutines: Top-level suspendin

kenel.tistory.com

해당 게시글의 #2에서 이어진다.

 

import kotlinx.coroutines.*

fun main() {
    println("Start")

    runBlocking {
        delay(5000) // 5초 대기
    }
    
    println("End")
}

/* 출력 결과
Start
End
*/

runBlocking은 이름에서 보듯, 현재 스레드를 Blocking(블로킹)하는 함수다. 그리고 runBlocking { ... } 영역 내의 코루틴 코드를 현재 스레드에서 실행한다. runBlocking { ... }이 끝남과 동시에 현재 스레드의 Blocking이 해제된다. 정리하면, Blocking된 스레드에서 runBlocking { ... } 속 코드만 예외적으로 실행한다. 그러나 우리가 코루틴을 쓰는 이유를 생각해보면 스레드를 Blocking해선 안 된다. 병렬 구조를 만들려고 Coroutine을 쓰는 것이니 말이다. 게다가 runBlocking 나머지 Coroutine builder와 달리 CoroutineScope의 확장 함수가 아니다. runBlocking의 정체는 대체 무엇인가? 

 

#2 코드의 동기성 및 비동기성

 

[Kotlin] Coroutines - 동기 코드, 비동기 코드

#1 동기 코드 vs 비동기 코드 #1-1 구분하기 코루틴을 제대로 사용하기 위해선 먼저, '동기 코드'와 '비동기 코드(= 코루틴 코드)'를 명확하게 구분할 줄 알아야 한다. 둘을 구분하는 기준은 쉽게 말

kenel.tistory.com

'동기 코드' 및 '비동기 코드'에 대한 개념을 읽어야 이 다음을 이해할 수 있다.

 

#3 runBlocking

#3-1 정체

launch, async, produce는 '비동기 코드'를 '동기 코드'의 영역에 둘 수 있게 만드는 포장지다. 반면, runBlocking은 '비동기 코드'를 '동기 코드' 만드는 포장지다. 1초 기다리는 동작을 수행하는 delay(1000)이라는 '비동기 코드'를 예로 들어 보겠다. 이 함수를 '동기 코드'의 영역에 둘 순 없다. 이 세상에 불가능이란 없다지만, 적어도 여기에선 Kotlin 컴파일러가 퇴짜를 놓기에 불가능하다. 이 때 runBlocking { ... }으로 delay(1000)을 감싸면, 컴파일러님이 해당 코드를 허락해준다.
 

#3-2 비동기 코드가 동기 코드 영역에 존재할 수 있는 기제

#3-1의 '컴파일러의 허락'은 runBlocking이 현재 스레드를 Blocking(블로킹)한다는 것에 근거된 허락이다. 스레드를 Block(멈춤)함으로써 runBlocking { ... }의 안과 밖에 '순차성'을 부여했기 때문이다. runBlocking이 시작되면(= '{' 뒤부터) 현재 스레드가 부분적으로 Blocking된다. 따라서 runBlocking { ... } 안에 들어오기 전의 작업이 runBlocking { ... } 내에서 수행되지 않을 것임이 보장된다. 부분적으로 Blocking된다는 말은,runBlocking { ... } 밖에 있는 부분이 Blocking된다는 것이다. runBlocking { ... } 안에 있는 코드들만 예외적으로 실행된다.
 
runBlocking이 끝나면(= '}'를 만나면) 스레드에 가했었던 부분적 Blocking을 해제해야 한다. 그런데 그 전에 수행되는 동작이 있다. 바로, runBlocking 내의 모든 코루틴이 종료될 때까지 기다리는 것이다. 즉, join, await, receive()가 암시적으로 수행된다고 보면 된다. 것이다. 그래야 runBlocking 내부의 작업이 runBlocking { ... } 밖에 있는 작업과 동시 실행된다거나 하는 일이 없을 것이기 때문이다. 즉, runBlocking { ... } 밖이 동기 코드의 영역임을 보장할 수 있기 때문이다. 현재 스레드의 입장에서 보면, 동기 코드 A → B → runBlocking { ... } → E가 순차적이며 연속적으로 실행된다. 스레드 입장에선 runBlocking { ... }이 동기 코드로 보이는 것이다.
 

#3-3 runBlocking의 용도

runBlocking의 실용성은 다른 Coroutine builder들에 비하면 없어보인다. 앞서 말했듯, 우리는 병렬 구조를 만들려고 Coroutine을 쓰는 것이니 말이다. 그래서인지, runBlocking은 대부분 코드 테스트, 디버깅 용도로 쓰인다고 한다. 동기 코드의 영역을 해치지 않으면서 간편하게 코루틴 코드를 사용할 수 있기 때문이다.

#3-4 여담

여담으로, runBlocking를 '비동기 코드 영역'에서 쓰는 건 쓸 데 없는 짓이다. 에러가 나는 것은 아니지만, runBlocking의 역할을 생각하면 당연하다. 동기 코드의 영역에 비동기 코드는 올 수 없지만, 비동기 코드 영역에 동기 코드는 얼마든지 허용되기 때문이다. 따라서, runBlocking은 의미가 없다. runBlocking은 일반적으로 '동기 코드의 영역'에서만 사용하는 함수라고 보면 된다.

 

#4 요약

스레드 입장에서 runBlocking { ... }은 동기 코드로 보인다. 실제로도 그렇다.