#1 이전 글
해당 게시글의 #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(launch(), asycn(), produce())와 달리 CoroutineScope의 확장 함수가 아니다. runBlocking의 정체는 대체 무엇인가?
#2 코드의 동기성 및 비동기성
'동기 코드' 및 '비동기 코드'에 대한 개념을 읽어야 이 다음을 이해할 수 있다.
#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은 일반적으로 '동기 코드의 영역'에서만 사용하는 함수라고 보면 된다.
#4 주의점
import kotlinx.coroutines.*
// 코드 1
fun main() {
println("main() is running on ${Thread.currentThread().name}")
runBlocking {
println("runBlocking is running on ${Thread.currentThread().name}")
launch {
delay(3000)
println("launch() is running on ${Thread.currentThread().name}")
}
}
// ...
}
/* ↑ ↑ ↑ 출력 결과
main() is running on main
runBlocking is running on main
launch() is running on main
*/
---
// 코드 2
fun main() {
println("main() is running on ${Thread.currentThread().name}")
runBlocking {
println("runBlocking is running on ${Thread.currentThread().name}")
CoroutineScope(Dispatchers.Default).launch {
println("launch() is running on ${Thread.currentThread().name}")
}
}
// ...
}
/* ↑ ↑ ↑ 출력 결과
main() is running on main
runBlocking is running on main
launch() is running on DefaultDispatcher-worker-1
*/
'코드 1'과 '코드 2'는 서로 다른 코드다. 출력 결과도 서로 다른 모습을 보여준다. 코드 1과 2 모두 runBlocking { ... }으로 runBlocking이 위치한 스레드 (여기서는 main()을 실행하는 스레드(이하 메인 스레드))의 작동을 Block한다는 점은 같다. 하지만 코드 1의 launch()가 runBlocking의 자식으로서 메인 스레드풀에서 실행되는 반면, 코드 2의 launch()는 새로운 코루틴 스코프에서 새로운 스레드를 할당받아 완전히 독립적인 작업을 수행한다.
#5 요약
스레드 입장에서 runBlocking { ... }은 동기 코드로 보인다. 실제로도 그렇다.
'깨알 개념 > Kotlin' 카테고리의 다른 글
[Kotlin] Couroutine - 스레드(Thread)와 스레드 풀(Thread Pool) (0) | 2024.02.15 |
---|---|
[Kotlin] Coroutines - suspend 키워드 (0) | 2024.02.14 |
[Kotlin] Coroutines - Coroutine builder (0) | 2024.02.12 |
[Kotlin] Coroutines - CoroutineScope, CoroutineContext (0) | 2024.02.10 |
[Kotlin] Coroutines - Suspend vs Block (0) | 2024.02.09 |