Coroutine - 구조화된 동시성과 부모-자식 관계(3): `runBlocking()` 과 `launch() 차이`
runBlocking()
함수의 launch()
함수는 모두 코루틴 빌더 함수이지만 호출부의 스레드를 사용하는 방법에 차이가 있다.
소스는 github 에 있습니다.
목차
- 1.
runBlocking()
함수의 동작 방식 - 2. runBlocking 코루틴의 하위에 생성된 코루틴의 동작
- 3.
runBlocking()
함수와launch()
함수의 동작 차이 - 4.
runBlocking()
vslaunch()
- 정리하며..
- 참고 사이트 & 함께 보면 좋은 사이트
1. runBlocking()
함수의 동작 방식
runBlocking()
은 코루틴을 시작하면서 호출 스레드를 차단(blocking) 한다는 특징을 가진 코루틴 빌더 함수이다.
(= 코루틴을 시작하면서 현재 스레드를 차단함)
runBlocking()
함수가 호출되면 새로운 코루틴인 runBlocking 코루틴이 실행되는데, 이 코루틴은 실행이 완료될 때까지 호출부의 스레드를 차단(block) 하고 사용한다.
기본 동작: 호출부 스레드(main) 를 점유하는 예시
package chap07
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
delay(1000L)
println("[${Thread.currentThread().name}] 코루틴 종료")
}
[main @coroutine#1] 코루틴 종료
runBlocking()
호출 시 호출 스레드인 메인 스레드(main) 에서 실행되는 runBlocking 코루틴이 생성됨- 이 코루틴은 1초 간 대기 후 실행 스레드를 출력하고 실행이 완료됨
- runBlocking 이 실행되는 동안 해당 스레드는 차단(blocking) 되어 다른 작업을 수행할 수 없음
- 코루틴이 완료되면 메인 스레드도 종료되며, 이는 일반적인 프로그램 종료 흐름임
위 코드에서 runBlocking 코루틴은 실행되는 동안 메인 스레드를 점유하고 사용한다.
하지만 runBlocking 코루틴은 작업 실행 시 호출부의 스레드를 사용하지 않고, 차단만 할 수도 있다.
Dispatcher 를 지정하면 실행되는 스레드는 변경되지만, 호출한 스레드는 여전히 차단된다.
Dispatcher 지정 시: 실행 스레드는 다르지만 호출부 스레드는 여전히 차단되는 예시
package chap07
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit>(Dispatchers.IO) {
delay(1000L)
println("[${Thread.currentThread().name}] 코루틴 종료")
}
[DefaultDispatcher-worker-1 @coroutine#1] 코루틴 종료
- Dispatchers.IO 를 지정했기 때문에 runBlocking 코루틴은 Dispatchers.IO 를 사용해 별도의 백그라운드 스레드(DefaultDispatcher-worker-1) 에서 실행됨
- 하지만
runBlocking()
함수를 호출한 스레드인 메인 스레드는 여전히 차단되어 runBlocking 코루틴이 완료될 때까지 대기함
위 그림에서 일시 중단과 재개 실행 시에 실행 스레드가 변할 수 있지만 여기서는 runBlocking 코루틴이 DefaultDispatcher-worker-1 스레드만 사용하여 실행되는 상황을 가정한다.
runBlocking()
함수가 호출된 스레드와 다른 스레드에서 runBlocking 코루틴이 실행되더라도 해당 코루틴이 실행되는 동안runBlocking()
함수를 호출한 스레드는 차단됨- 차단이 풀리는 시점은 runBlocking 코루틴이 실행 완료될 때임
runBlocking 함수의 차단은 스레드 블로킹에서의 차단과 다르다.
- 스레드 블로킹
- 스레드가 어떤 작업에도 사용할 수 없도록 차단되는 것을 의미
runBlocking()
함수의 차단- runBlocking 코루틴과 그 자식 코루틴을 제외한 다른 작업이 스레드를 사용할 수 없음을 의미
- 즉, runBlocking 코루틴과 그 자식만 스레드를 사용할 수 있도록 제한
2. runBlocking 코루틴의 하위에 생성된 코루틴의 동작
runBlocking 코루틴은 호출한 스레드를 블로킹하면서 점유한다.
이 말은 곧 그 하위에서 생성된 코루틴들도 해당 스레드를 공유할 수 있다는 뜻이다.
package chap07
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis() // 시작 시간
launch {
delay(1000L)
println("[${Thread.currentThread().name}] launch 코루틴 종료")
}
delay(2000L)
println("[${Thread.currentThread().name}] runBlocking 코루틴 종료")
println("${getElapsedTime(startTime)}")
}
fun getElapsedTime(startTime: Long): String {
return "지난 시간: ${System.currentTimeMillis() - startTime}ms"
}
[main @coroutine#2] launch 코루틴 종료
[main @coroutine#1] runBlocking 코루틴 종료
지난 시간: 2009ms
- launch 코루틴은 runBlocking 코루틴의 자식 코루틴으로 생성됨
- 두 코루틴 모두 메인 스레드(main) 을 사용하고 있음
- launch 코루틴은 1초 후에 실행되고, runBlocking 코루틴은 전체 2초 대기 후 종료되며, 출력 결과에 스레드 이름이 main 으로 동일하게 표시됨
- 즉, runBlocking 이 호출한 메인 스레드를 자식 코루틴도 사용할 수 있음
runBlocking()
이 호출되면 메인 스레드가 블로킹됨- 그 안에서 생성된 launch 코루틴도 메인 스레드에서 실행됨
- 전체 실행 시간은 약 2초로, runBlocking 실행 시간과 일치함
3. runBlocking()
함수와 launch()
함수의 동작 차이
runBlocking()
과 launch()
는 모두 코루틴을 시작할 수 있는 함수이다.
하지만 스레드 차단 여부와 실행 타이밍에서 큰 차이가 있다.
runBlocking()
은 실행 시 호출부 스레드를 차단한다.
runBlocking()
은 블로킹을 일으키는 일반적인 코드와 코루틴 사이의 연결점 역할을 하기 위해 만들어졌기 때문에, main 함수 또는 테스트 코드에서만 사용하는 것을 권장한다.
코루틴 안에서 다시runBlocking()
을 호출하는 것은 지양하는 것이 좋다.아래는 예시를 위한 코드일 뿐이다.
아래는 runBlocking 코루틴 내부에서 runBlocking 함수가 호출되는 경우이다.
package chap07
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
// 하위 runBlocking 코루틴
// 바깥쪽 runBlocking 코루틴이 차단한 스레드를 사용할 수 있기 때문에 메인 스레드 상에서 실행되며,
// 실행되는 동안 메인 스레드를 차단함
runBlocking {
delay(1000L)
println("[${Thread.currentThread().name}] 하위 코루틴 종료")
}
println(getElapsedTime(startTime)) // 지난 시간 출력
}
fun getElapsedTime(startTime: Long): String {
return "지난 시간: ${System.currentTimeMillis() - startTime}ms"
}
[main @coroutine#2] 하위 코루틴 종료
지난 시간: 1013 ms
- runBlocking 코루틴은 호출부의 스레드(여기서는 메인 스레드)를 차단하고 점유한다.
- 따라서 하위 runBlocking 이 실행되는 동안 메인 스레드는 다른 작업을 할 수 없다.
- 하위 코루틴이 모두 완료된 뒤에서 “지난 시간”이 출력되며, 약 1초가 소요된 것을 확인할 수 있다.
launch()
은 실행 시 호출부 스레드를 차단하지 않는다.
따라서 launch 코루틴이 delay 와 같은 작업으로 실제로 스레드를 사용하지 않는 동안 스레드를 다른 작업에 사용될 수 있다.
위 코드의 하위 runBlocking 을 launch 로 변경해보자.
package chap07
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
// 하위 launch 코루틴
// 호출부의 스레드를 차단하고 실행되는 것이 아니기 때문에 즉시 실행되지 않고,
// runBlocking 코루틴이 메인 스레드를 양보하고 나서야 메인 스레드로 보내져서 실행됨
launch {
delay(1000L)
println("[${Thread.currentThread().name}] 하위 코루틴 종료")
}
println(getElapsedTime(startTime)) // 지난 시간 출력
}
fun getElapsedTime(startTime: Long): String {
return "지난 시간: ${System.currentTimeMillis() - startTime}ms"
}
지난 시간: 0 ms
[main @coroutine#2] 하위 코루틴 종료
- launch 는 호출부의 스레드를 차단하지 않기 때문에 바로 “지난 시간” 이 출력된다.
- 이후 launch 코루틴이 스레드를 사용할 수 있을 때(여기서는 메인 스레드가 자유로워 졌을 때) 실행된다.
- launch 코루틴이 delay 로 인해 1초간 대기하는 동안 메인 스레드는 다른 작업이 자유롭게 사용할 수 있다.
4. runBlocking()
vs launch()
구분 | runBlocking() | launch() |
---|---|---|
스레드 차단 여부 | O (blocking) | X (non-blocking) |
사용 목적 | 일반 코드와 코루틴 간 연결 | 비동기 작업 실행 |
사용 위치 | main 함수, 테스트 코드 | 코루틴 컨텍스트 어디서나 |
하위 코루틴 실행 | 즉시 실행 & 차단 | 예약 실행 & 비차단 |
정리하며..
runBlocking()
코루틴 빌더는 코루틴이 완료될 때까지 호출 스레드를 차단하며, 블로킹 방식으로 코루틴을 실행한다.launch()
코루틴 빌더는 호출 스레드를 차단하지 않고 비동기적으로 코루틴을 실행한다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 조세영 저자의 코틀린 코루틴의 정석을 기반으로 스터디하며 정리한 내용들입니다.