안드로이드 개발자 노트
[코루틴의 정석] 10장. 코루틴의 이해 본문
10.1. 서브루틴과 코루틴
10.1.1. 루틴과 서브루틴
루틴은 특정한 일을 하기 위한 일련의 처리 과정이며, 함수 또는 메서드라고 부른다.
서브루틴은 함수의 하위에서 실행되는 함수를 말하며, 루틴에서 서브루틴이 호출되면 서브루틴이 완료될 때까지 루틴이 아무런 작업을 할 수 없다.
10.1.2. 서브루틴과 코루틴의 차이
한 번 실행되면 실행이 완료될 때까지 스레드를 사용하는 서브루틴과 다르게 코루틴은 스레드 사용 권한을 양보하며 함께 실행된다.
10.2. 코루틴의 스레드 양보
스레드를 양보하는 주체는 코루틴으로, CoroutineDispatcher는 코루틴이 스레드를 양보하도록 강제하지 못한다.
코루틴에서 스레드 양보를 위한 함수가 호출되지 않는다면 코루틴은 실행 완료될 때까지 스레드를 점유한다.
10.2.1. delay 일시 중단 함수를 통해 알아보는 스레드 양보
코루틴이 delay 함수를 호출하면 코루틴은 사용하던 스레드를 양보하고 설정된 시간 동안 코루틴을 일시 중단시킨다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
repeat(10) { repeatTime ->
launch {
delay(1000L) // 1초 동안 코루틴 일시 중단
println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료") // 지난 시간과 함께 "코루틴 실행 완료" 출력
}
}
}
/*
// 결과:
[지난 시간: 1014ms] 코루틴0 실행 완료
[지난 시간: 1015ms] 코루틴1 실행 완료
[지난 시간: 1015ms] 코루틴2 실행 완료
[지난 시간: 1015ms] 코루틴3 실행 완료
[지난 시간: 1015ms] 코루틴4 실행 완료
[지난 시간: 1015ms] 코루틴5 실행 완료
[지난 시간: 1015ms] 코루틴6 실행 완료
[지난 시간: 1016ms] 코루틴7 실행 완료
[지난 시간: 1016ms] 코루틴8 실행 완료
[지난 시간: 1016ms] 코루틴9 실행 완료
*/
각 코루틴은 시작하자마자 delay 함수로 1초 동안 메인 스레드 사용을 양보한다. 이 때문에 하나의 코루틴이 실행된 후 바로 다음 코루틴이 실행될 수 있으며, 10개의 코루틴이 거의 동시에 시작된다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
repeat(10) { repeatTime ->
launch {
Thread.sleep(1000L) // 1초 동안 스레드 블로킹(코루틴의 스레드 점유 유지)
println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료") // 지난 시간과 함께 "코루틴 실행 완료" 출력
}
}
}
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"
/*
// 결과:
[지난 시간: 1007ms] 코루틴0 실행 완료
[지난 시간: 2014ms] 코루틴1 실행 완료
[지난 시간: 3016ms] 코루틴2 실행 완료
[지난 시간: 4019ms] 코루틴3 실행 완료
[지난 시간: 5024ms] 코루틴4 실행 완료
[지난 시간: 6030ms] 코루틴5 실행 완료
[지난 시간: 7036ms] 코루틴6 실행 완료
[지난 시간: 8041ms] 코루틴7 실행 완료
[지난 시간: 9045ms] 코루틴8 실행 완료
[지난 시간: 10051ms] 코루틴9 실행 완료
*/
Thread.sleep 함수는 delay 함수와 유사하게 일정 시간 동안 대기하는 데 사용되지만 대기 시간 동안 스레드를 블로킹시키기 때문에 모든 코루틴이 실행되는 데 걸리는 시간은 10초가량이다.
10.2.2. join과 await의 동작 방식 자세히 알아보기
Job의 join 함수나 Deferred의 await 함수가 호출되면 해당 함수를 호출한 코루틴은 스레드를 양보하고 join 또는 await의 대상이 된 코루틴 내부의 코드가 완료될 때까지 일시 중단한다.
fun main() = runBlocking<Unit> {
val job = launch {
println("1. launch 코루틴 작업이 시작됐습니다")
delay(1000L) // 1초간 대기
println("2. launch 코루틴 작업이 완료됐습니다")
}
println("3. runBlocking 코루틴이 곧 일시 중단 되고 메인 스레드가 양보됩니다")
job.join() // job 내부의 코드가 모두 실행될 때까지 메인 스레드 일시 중단
println("4. runBlocking이 메인 스레드에 분배돼 작업이 다시 재개됩니다")
}
/*
// 결과:
3. runBlocking 코루틴이 곧 일시 중단 되고 메인 스레드가 양보됩니다
1. launch 코루틴 작업이 시작됐습니다
2. launch 코루틴 작업이 완료됐습니다
4. runBlocking이 메인 스레드에 분배돼 작업이 다시 재개됩니다
*/
runBlocking 코루틴과 launch 코루틴은 다음 순서로 동작한다.
- 처음 메인 스레드를 점유하는 것은 runBlocking 코루틴이다. runBlocking 코루틴은 launch 함수를 호출해 launch 코루틴을 생성하지만 launch 코루틴 생성 후에도 runBlocking 코루틴이 계속해서 메인 스레드를 점유하기 때문에 launch 코루틴은 실행 대기 상태에 머문다. 이후 runBlocking 코루틴이 3번 로그를 출력하고 job.join()을 실행하면 비로소 메인 스레드가 양보된다.
- 이때 자유로워진 메인 스레드에 launch 코루틴이 보내져 실행된다. launch 코루틴은 1번 로그를 출력하고 이어서 delay 일시 중단 함수를 호출해 메인 스레드를 양보한다. 하지만 runBlocking 코루틴은 job.join()에 의해 launch 코루틴이 실행 완료될 때까지 재개되지 못하므로 실행되지 못한다.
- launch 코루틴은 delay에 의한 일시 중단 시간 1초가 끝나고 재개되며, 2번 로그가 출력되고 실행이 완료된다.
- launch 코루틴의 실행이 완료되면 runBlocking 코루틴은 재개돼 4번 로그를 출력한다.
10.2.3. yield 함수 호출해 스레드 양보하기
yield 함수는 직접 스레드 양보를 실행하는 방법이다.
fun main() = runBlocking<Unit> {
val job = launch {
while (this.isActive) {
println("작업 중")
//yield() //runBlocking에게 스레드 양보
}
}
delay(100L) // 100밀리초 대기(스레드 양보)
job.cancel() // 코루틴 취소
}
/*
// 결과:
...
작업 중
작업 중
작업 중
...
*/
launch 코루틴이 스레드를 점유하고 있으므로 runBlocking 코루틴에 있는 job.cancel()은 실행되지 못한다.
10.3. 코루틴의 실행 스레드
10.3.1. 코루틴의 실행 스레드는 고정이 아니다
코루틴이 일시 중단 후 재개되면 CoroutineDispatcher 객체는 재개된 코루틴을 다시 스레드에 할당한다. 이때 CoroutineDispatcher 객체는 코루틴을 자신이 사용할 수 있는 스레드 중 하나에 할당하는 데 이 스레드는 코루틴이 일시 중단 전에 실행되던 스레드와 다를 수 있다.
fun main() = runBlocking<Unit> {
val dispatcher = newFixedThreadPoolContext(2, "MyThread")
launch(dispatcher) {
repeat(5) {
println("[${Thread.currentThread().name}] 코루틴 실행이 일시 중단 됩니다") // launch 코루틴을 실행 중인 스레드를 출력한다.
delay(100L) // delay 함수를 통해 launch 코루틴을 100밀리초간 일시 중단한다.
println("[${Thread.currentThread().name}] 코루틴 실행이 재개 됩니다") // launch 코루틴이 재개되면 코루틴을 실행 중인 스레드를 출력한다.
}
}
}
/*
// 결과:
[MyThread-1 @coroutine#2] 코루틴 실행이 일시 중단 됩니다
[MyThread-2 @coroutine#2] 코루틴 실행이 재개 됩니다
[MyThread-2 @coroutine#2] 코루틴 실행이 일시 중단 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 재개 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 일시 중단 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 재개 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 일시 중단 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 재개 됩니다
[MyThread-1 @coroutine#2] 코루틴 실행이 일시 중단 됩니다
[MyThread-2 @coroutine#2] 코루틴 실행이 재개 됩니다
*/
10.3.2. 스레드를 양보하지 않으면 실행 스레드가 바뀌지 않는다
코루틴의 실행 스레드가 바뀌는 시점이 코루틴이 재개될 때이다. 코루틴이 스레드를 양보하지 않아 일시 중단 될 일이 없다면 실행 스레드가 바뀌지 않는다.
fun main() = runBlocking<Unit> {
val dispatcher = newFixedThreadPoolContext(2, "MyThread")
launch(dispatcher) {
repeat(5) {
println("[${Thread.currentThread().name}] 스레드를 점유한채로 100밀리초간 대기합니다")
Thread.sleep(100L) // 스레드를 점유한 채로 100 밀리초 동안 대기
println("[${Thread.currentThread().name}] 점유한 스레드에서 마저 실행됩니다")
}
}
}
/*
// 결과:
[MyThread-1 @coroutine#2] 스레드를 점유한채로 100밀리초간 대기합니다
[MyThread-1 @coroutine#2] 점유한 스레드에서 마저 실행됩니다
[MyThread-1 @coroutine#2] 스레드를 점유한채로 100밀리초간 대기합니다
[MyThread-1 @coroutine#2] 점유한 스레드에서 마저 실행됩니다
[MyThread-1 @coroutine#2] 스레드를 점유한채로 100밀리초간 대기합니다
[MyThread-1 @coroutine#2] 점유한 스레드에서 마저 실행됩니다
[MyThread-1 @coroutine#2] 스레드를 점유한채로 100밀리초간 대기합니다
[MyThread-1 @coroutine#2] 점유한 스레드에서 마저 실행됩니다
[MyThread-1 @coroutine#2] 스레드를 점유한채로 100밀리초간 대기합니다
[MyThread-1 @coroutine#2] 점유한 스레드에서 마저 실행됩니다
*/
요약
- 프로그래밍에서는 루틴을 '특정한 일을 처리하기 위한 일련의 명령'이라는 뜻으로 사용하고 있으며, 이런 일련의 명령을 함수 또는 메서드라고 한다.
- 서브루틴은 루틴의 하위에서 실행되는 루틴이다. 즉, 함수 내부에서 호출되는 함수를 서브루틴이라고 한다.
- 서브루틴은 한 번 실행되면 끝까지 실행되는 반면에 코루틴은 서로 간에 스레 드 사용 권한을 양보하며 함께 실행된다.
- delay 함수는 스레드를 양보하고 일정 시간 동안 코루틴을 일시 중단시킨다
- join과 await 함수를 호출한 코루틴은 join이나 await의 대상이 된 코루틴의 작업이 완료될 때까지 스레드를 양보하고 일시 중단한다
- yield 함수는 스레드 사용 권한을 명시적으로 양보하고자 할 때 사용한다.
- 코루틴은 협력적으로 동작한다. 코루틴은 스레드 사용 권한을 양보함으로써 스레드가 실제로 사용되지 않는 동안 다른 코루틴이 스레드를 사용할 수 있도록 한다.
- 코루틴이 스레드를 양보하면 코루틴은 일시 중단되며, 재개될 때 CoroutineDispatcher 객체를 통해 다시 스레드에 보내진다. CoroutineDispatcher 객체는 코루틴을 쉬고 있는 스레드 중 하나에 보내므로 일시 중단 전의 스레드와 다른 스레드에서 재개될 수 있다.
- 코루틴이 스레드를 양보하지 않으면 실행 스레드가 바뀌지 않는다.
- 코루틴 내부에서 Thread.sleep 함수를 사용하면 코루틴이 대기하는 시간 동안 스레드를 양보하지 않고 블로킹한다
'Kotlin > 코루틴의 정석' 카테고리의 다른 글
[코루틴의 정석] 11-1장. 코루틴 심화 (0) | 2024.10.01 |
---|---|
[코루틴의 정석] 9장. 일시 중단 함수 (0) | 2024.09.20 |
[코루틴의 정석] 8장. 예외처리 (0) | 2024.09.18 |