서브루틴과 코루틴
1. 루틴과 서브루틴
- 프로그래밍 과점에서 루틴은 `특정한 일을 처리하기 위한 일련의 명령`이라는 뜻으로 사용
- 이런 일련의 명령을 함수 또는 메서드라고 지칭
- 서브루틴은 함수 내에서 함수가 호출될 경우 호출된 함수
- 간단하게 설명하면 서브루틴이란 함수의 하위(sub)에서 실행되는 함수를 지칭
- 서브루틴은 한 번 호출되면 끝까지 실행됨
- 따라서 루틴에 의해 서브루틴이 호출되면 루틴을 실행하던 쓰레드는 서브루틴을 실행하는 데 사용되어 서브루틴의 실행이 완료될 때까지 루틴은 다른 작업을 할 수 없음 (blocking)
2. 서브루틴과 코루틴의 차이
- 서브루틴과 달리 코루틴은 함께(co) 실행되는 루틴으로 서로 간에 쓰레드 사용을 양보하며 함께 실행됨
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
launch { | |
while (true) { | |
println("자식 코루틴에서 작업 실행 중") | |
yield() // 쓰레드 사용 권한 양보 | |
} | |
} | |
while (true) { | |
println("부모 코루틴에서 작업 실행 중") | |
yield() // 쓰레드 사용 권한 양보 | |
} | |
} |

부연 설명
- 부모 코루틴은 while 문을 통해 `부모 코루틴에서 작업 실행 중`을 출력하고 쓰레드 사용 권한을 양보하는 yield 함수를 호출하는 동작을 무한 반복
- 자식 코루틴도 while 문을 통해 `자식 코루틴에서 작업 실행 중`을 출력하고 yield 함수를 호출해 쓰레드 사용 권한을 양보하는 동작을 무한 반복
- 각 코루틴이 쓰레드 사용 권한을 양보할 때마다 쓰레드가 필요한 다른 코루틴이 쓰레드 사용 권한을 가져가 실행
- 서브루틴과 달리 코루틴은 서로 간에 협력적으로 동작하는 것이 핵심
코루틴의 쓰레드 양보
- 쓰레드에 코루틴을 할당해 실행되도록 만드는 주체는 CoroutineDispatcher 객체이지만 쓰레드를 양보하는 주체는 코루틴
- CoroutineDispatcher는 코루틴이 쓰레드를 양보하도록 강제하지 못함
- 코루틴이 쓰레드를 양보하려면 코루틴에서 직접 쓰레드 양보를 위한 함수를 호출해야 하며 쓰레드를 양보를 일으키는 대표적인 일시 중단 함수들은 다음과 같음
- delay
- join & await
- yield
1. delay 일시 중단 함수를 통해 알아보는 쓰레드 양보
- 작업을 일정 시간 동안 일시 중단할 경우 delay 일시 중단 함수 사용
- 코루틴이 delay 함수 호출 시 코루틴은 사용하던 쓰레드를 양보하고 설정된 시간 동안 코루틴을 일시 중단시킴
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
repeat(10) { repeatTime -> | |
launch { | |
delay(1000L) | |
println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료") // 자식 시간과 함께 "코루틴 실행 완료" 출력 | |
} | |
} | |
} |

부연 설명
- delay 함수는 메인 쓰레드 사용을 양보하기 때문에 10개의 코루틴이 거의 동시에 시작되는 것이 특징
- 만약 delay 함수를 호출했을 때 쓰레드 양보가 일어나지 않았다면 작업을 모두 실행하는 데 10초가 걸렸을 것
- 실제로 delay 대신 Thread.sleep(1000L)을 사용할 경우 10초가 걸리는 것을 확인 가능
- Thread.sleep 함수는 쓰레드를 대기 시간 동안 점유하며 블로킹시키기 때문
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
repeat(10) { repeatTime -> | |
launch { | |
Thread.sleep(1000L) | |
println("[${getElapsedTime(startTime)}] 코루틴${repeatTime} 실행 완료") // 자식 시간과 함께 "코루틴 실행 완료" 출력 | |
} | |
} | |
} |

2. join과 await의 동작 방식 자세히 알아보기
- Job의 join 함수나 Deferred의 await 함수가 호출되면 해당 함수를 호출한 코루틴은 쓰레드를 양보하고
- join 또는 await의 대상이 된 코루틴 내부의 코드가 실행 완료될 때까지 일시 중지됨
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val job = launch { | |
println("1. launch 코루틴 작업이 시작됐습니다") | |
delay(1000L) | |
println("2. launch 코루틴 작업이 완료됐습니다") | |
} | |
println("3. runBlocking 코루틴이 곧 일시 중단 되고 메인 쓰레드가 양보됩니다.") | |
job.join() | |
println("4. runBlocking이 메인 쓰레드에 분배돼 작업이 다시 재개됩니다.") | |
} |

부연 설명
- runBlocking 코루틴과 launch 코루틴은 단일 쓰레드인 메인 쓰레드에서 실행되기 때문에 하나의 코루틴이 쓰레드를 양보하지 않으면 다른 코루틴이 실행되지 못하기 때문에 각 코루틴은 다음 순서로 동작함
- 처음 메인 쓰레드를 점유하는 것은 runBlocking 코루틴이며 해당 코루틴은 launch 함수를 호출해 launch 코루틴을 생성하지만 launch 코루틴 생성 후에도 runBlocking 코루틴이 계속해서 메인 쓰레드를 점유하기 때문에 launch 코루틴은 실행 대기 상태에 머뭄
- 3번 로그 출력 후 job.join()을 실행하면 비로소 메인 쓰레드가 양보되며 launch 코루틴이 실행되어 1번 로그를 출력하고 이어서 delay 일시 중단 함수를 호출해 메인 쓰레드를 양보
- 하지만 runBlocking 코루틴은 job.join()에 의해 launch 코루틴이 실행 완료될 때까지 재개되지 못하므로 실행되지 못함
- launch 코루틴은 1초 동안 일시 중단 시간 후 지개되어 2번 로그가 출력되어 실행이 완료됨
- launch 코루틴의 실행이 완료되면 runBlocking 코루틴은 재개돼 4번 로그를 출력
3. yield 함수 호출해 쓰레드 양보하기
- 앞서 다룬 delay, join 같은 일시 중단 함수들은 쓰레드 양보를 직접 호출하지 않아도 작업을 위해 내부적으로 쓰레드 양보를 일으키는 반면 yield 함수는 개발자가 명시적으로 쓰레드 양보를 실행할 때 사용하는 함수
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val job = launch { | |
while (this.isActive) { | |
println("작업 중") | |
yield() // 쓰레드 양보 명시적으로 | |
} | |
} | |
delay(100L) | |
job.cancel(); | |
} |

부연 설명
- runBlocking 코루틴이 delay 일시 중단 함수를 호출해 메인 쓰레드를 양보하면 launch 코루틴이 메인 쓰레드를 점유하고 양보하지 않기 때문에 yield() 함수를 호출하지 않을 경우 runBlocking 코루틴의 나머지 코드인 job.cancel()은 실행되지 못함
- launch 코루틴은 while 문 내부에서 yield()를 호출해 명시적으로 쓰레드를 양보
- launch 코루틴이 양보한 메인 쓰레드는 runBlocking 코루틴의 나머지 코드를 실행하는 데 사용돼 job.cancel()이 호출됨
코루틴의 실행 쓰레드
1. 코루틴의 실행 쓰레드는 고정이 아니다
- 앞서 설명했다시피 코루틴이 일시 중단 후 재개되면 CoroutineDispatcher 객체는 재개된 코루틴을 다시 쓰레드에 할당함
- 이때 CoroutineDispatcher 객체는 코루틴을 자신이 사용할 수 있는 쓰레드 중 하나에 할당하는데 해당 쓰레드는 코루틴이 일시 중단 전에 실행되던 쓰레드와 다를 수 있음
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val dispatcher = newFixedThreadPoolContext(2, "MyThread") | |
launch(dispatcher) { | |
repeat(5) { | |
println("[${Thread.currentThread().name}] 코루틴 실행이 일시 중단 됩니다") | |
delay(100L) | |
println("[${Thread.currentThread().name}] 코루틴 실행이 재개 됩니다") | |
} | |
} | |
} |

부연 설명
- newFixedThreadPoolContext 함수를 통해 MyThread-1, MyThread-2 쓰레드로 구성된 쓰레드 풀을 사용하는 CoroutineDispatcher 객체를 생성함
- coroutine#2는 MyThread-1 쓰레드에서 실행될 때도 있고, MyThread-2 쓰레드에서 실행될 때도 있는 것을 확인 가능
- coroutine#2의 실행 쓰레드가 바뀌는 이유는 coroutine#2가 재개될 때 CoroutineDispatcher 객체가 자신이 사용할 수 있는 쓰레드 중 하나에 coroutine#2를 보내기 때문
- 코루틴의 실행 쓰레드가 변경되는 시점은 재개 시점
2. 쓰레드를 양보하지 않으면 실행 쓰레드가 바뀌지 않는다
- 앞서 설명했다시피 코루틴의 실행 쓰레드가 바뀌는 시점은 코루틴이 재개되는 시점
- 코루틴이 쓰레드 양보를 하지 않아 일시 중단될 일이 없다면 실행 쓰레드가 바뀌지 않음
- 따라서 delay 대신 Thread.sleep() 함수를 사용하면 코루틴은 대기 시간 동안 쓰레드를 양보하지 않아 실행 쓰레드가 바뀌지 않음
- 코루틴이 쓰레드를 양보하지 않으면 코루틴을 사용하는 이점이 모두 사라지게 되므로 지양해야 함
참고
코틀린 코루틴의 정석 (조세영 저)
반응형
'Kotlin > 코틀린 코루틴의 정석' 카테고리의 다른 글
[Kotlin 코루틴] 코루틴 심화 (1) | 2024.11.29 |
---|---|
[Kotlin 코루틴] 일시 중단 함수 (0) | 2024.11.22 |
[Kotlin 코루틴] 예외 처리 (0) | 2024.11.22 |
[Kotlin 코루틴] 구조화된 동시성 (0) | 2024.11.21 |
[Kotlin 코루틴] CoroutineContext 정리 (0) | 2024.11.19 |