Coroutine
•
코루틴은 일시 중단할 수 있는 계산 인스턴스이다.
•
개념적으로는 스레드와 유사하게 코드 블록을 실행하며 나머지 코드와 병렬로 동작한다.
•
하지만 코루틴은 특정 스레드에 종속되지 않는다.
•
한 스레드에서 실행을 중단할 수 있으며 다른 스레드에서 다시 실행을 재개할 수 있다.
•
코루틴은 가벼운 스레드로 생각할 수 있지만 실제 사요 시 스레드와는 몇 가지 중요한 차이점이 있으며 이로 인해 스레드와는 상당히 다르게 동작한다.
예제 코드
fun main() = runBlocking { // this: CoroutineScope
launch { // 새로운 코루틴을 시작하고 계속 실행
delay(1000L) // 1초 동안 비차단 대기 (기본 시간 단위는 밀리초)
println("World!") // 대기 후 출력
}
println("Hello") // 이전 코루틴이 대기 중일 때 메인 코루틴은 계속 실행
}
// 출력:
// Hello
// World!
Kotlin
복사
•
launch
◦
코루틴 빌더이다.
◦
이것은 나머지 코드와 병렬로 새로운 코루틴을 실행하며 코드는 독립적으로 계속 실행된다.
•
delay
◦
특별한 일시 중단 함수이다.
◦
이것은 코루틴을 특정 시간 동안 중단시키며 해당 코루틴이 실행되는 스레드를 차단하지 않으므로 다른 코루틴이 해당 스레드를 사용하여 실행될 수 있다.
•
runBlocking
◦
코루틴 빌더이다.
◦
일반 함수인 main() 과 코루틴 코드를 연결해준다.
Unresolved reference: launch
Kotlin
복사
•
launch는 CoroutineScope에서만 선언되었기 때문에 만약 runBlocking 을 제거하거나 잊어버리면 launch 호출 시 오류가 발생한다.
•
runBlocking은 해당 스레드가 코루틴이 모두 완료될 때까지 차단된다는 뜻이다.
•
이 방식은 응용 프로그램의 가장 상위 레벨에서 자주 사용되며 실제 코드에서는 거의 사용되지 않는다.
•
스레드는 리소스가 많이 드는 자원이기 때문에 차단하는 것은 비효율적이다.
구조적 동시성
•
코루틴은 구조적 동시성이라는 원칙을 따른다.
•
즉, 새로운 코루틴은 특정 CoroutineScope 내에서만 시작할 수 있으며 이 스코프는 코루틴의 생명 주기를 한정한다.
•
실제 어플리케이션에서는 많은 코루틴을 실행하게 된다.
•
구조적 동시성은 이러한 코루틴이 누락되거나 메모리 누수가 발생하지 않도록 보장한다.
•
외부 스코프는 자식 코루틴이 모두 완료될 때까지 완료되지 않으며 코드의 모든 오류가 적절하게 보고되고 손실되지 않도록 보장한다.
함수 추출 리팩토링
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// 첫 번째 일시 중단 함수
suspend fun doWorld() {
delay(1000L)
println("World!")
}
Kotlin
복사
•
launch { … } 블록 내부의 코드를 별도의 함수로 추출해보자.
•
이 코드를 리팩토링하면 suspend 수정자가 있는 새로운 일시 중단 함수가 만들어진다.
•
일시 중단 함수는 일반 함수처럼 코루틴 내부에서 사용할 수 있으며 추가로 다른 일시 중단 함수(delay 같은)를 사용하여 코루틴 실행을 일시 중단할 수 있다.
스코프 빌더
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
Kotlin
복사
•
코루틴 스코프는 코루틴 빌더에 의해 제공되지만 coroutineScope 빌더를 사용하여 직접 선언할 수도 있다.
•
이는 코루틴 스코프를 생성하며 모든 자식 코루틴이 완료될 때까지 완료되지 않는다.
•
runBlocking과 coroutineScope 빌더는 둘 다 본문과 자식 코루틴이 완료될 때까지 기다린다는 점에서 유사하다.
•
주요 차이점은 runBlocking은 현재 스레드를 차단하여 기다리는 반면 coroutineScope는 일시 중단하여 스레드를 다른 용도로 사용할 수 있도록 한다.
•
이러한 차이로 인해 runBlocking은 일반 함수이고 coroutineScope는 일시 중단 함수이다.
스코프 빌더와 동시성
•
coroutineScope 빌더는 일시 중단 함수 안에서 여러 개의 동시 작업을 수행하는데 사용할 수 있다.
•
doWorld라는 일시 중단 함수 안에서 두 개의 동시 코루틴을 실행할 수 있다.
// doWorld가 순차적으로 실행되고, 그 후에 "Done"이 출력됨
fun main() = runBlocking {
doWorld()
println("Done")
}
// 두 섹션이 동시 실행됨
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
// 출력:
// Hello
// World 1
// World 2
// Done
Kotlin
복사
•
두 개의 launch { … } 블록 안의 코드가 동시에 실행되며 시작 후 1초가 지나면 “World 1” 이 먼저 출력되고 2초가 지나면 “World 2” 가 출력된다.
•
doWorld 내부의 coroutineScope 는 두 개의 코루틴이 모두 완료된 후에야 완료되므로 doWorld 가 변환되고 나서 “Done” 문자열이 출력된다.
명시적인 Job
val job = launch { // 새로운 코루틴을 시작하고 Job을 참조로 유지
delay(1000L)
println("World!")
}
println("Hello")
job.join() // 자식 코루틴이 완료될 때까지 대기
println("Done")
// 출력:
// Hello
// World!
// Done
Kotlin
복사
•
launch 코루틴 빌더는 Job 객체를 반환하며 이는 시작된 코루틴에 대한 핸들로 사용될 수 있고 이를 통해 명시적으로 코루틴의 완료를 기다릴 수 있다.
코루틴은 경량이다.
•
코루틴은 JVM 스레드보다 리소스를 덜 사용한다.
•
스레드를 사용할 때 JVM 메모리를 초과하여 프로그램이 종료될 수 있는 코드도 코루틴을 사용하면 리소스 한계를 초과하지 않는다.
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(50_000) { // 많은 코루틴 실행
launch {
delay(5000L)
print(".")
}
}
}
Kotlin
복사
•
만약 이 프로그램을 스레드를 사용하여 작성한다면(runBlocking을 제거하고 launch를 thread로, delay를 Thread.sleep으로 변경한다면) 많은 메모리를 소비하게 된다.
•
운영체제, JDK 버전 그리고 설정에 따라 메모리 부족 오류가 발생하거나 너무 많은 스레드를 동시에 실행하지 않도록 스레드가 천천히 시작될 수 있다.