[Android(Kotlin)] 코루틴(Coroutine) 다시 공부하기 - 2 (코루틴 기본)
카테고리: Android(Kotlin)
이전 게시물에서는 코루틴을 추적하고 제어하는 Scope를 알아보았다. 이번 게시물 부터는 제목에 걸맞게 코루틴 공식 가이드를 읽어보며 처음부터 차근 차근 알아보려고 한다.
코루틴 기본
코루틴은 일시 중지가 가능한 계산 인스턴스이다. 코드의 나머지 부분과 동시에 작동하는 코드 블록을 실행한다는 의미에서는 스레드와 개념적으로 유사하나 코루틴은 특정 스레드에 바인딩되지 않는다. 그래서 한 스레드에서 실행을 일시 중지하고 다른 슬드에서 다시 시작할 수 있다.
코루틴은 경량화된 스레드로 생각할 수 있지만 실제 사용을 스레드와 매우 다르게 만드는 중요한 차이점이 많이 있다.
다음의 코드를 실행해보자
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
위의 코드를 실행하면 다음과 같은 결과가 표시될 것이다.
Hello
World!
이제 이 코드가 무엇을 하는지 분석해 보자
launch
는 코루틴 빌더(coroutine builder)이다. 나머지 코드와 독립적으로 동시에 작동하는 새 코루틴을 실행한다. 그래서 Hello가 먼저 출력된 것이다.
delay
는 특별한 일시 중지 함수이다. 코루틴을 특정 시간 동안 일시 중단 한다.
코루틴을 일시 중단 하면 아래에 놓여있는 기본 스레드가 차단되지는 않지만 다른 코루틴의 작동과 코드에서 기본 스레드의 사용이 허용된다.(코루틴 안에서만 호출될 수 있다.)
runBlocking
또한 코루틴 빌더로 fun main()
(메인 함수)의 코루틴이 아닌 부분과 {}
내부의 코루틴이 있는 코드를 연결한다.
만약에 이 코드에서 runBlocking이 빠진다면, launch
는 코루틴 스코프에서만 선언되기 때문에 오류가 발생한다.
runBlocking
의 뜻은 내부의 모든 코루틴이 실행을 완료할 때까지 runBlocking을 실행하는 스레드(여기서는 메인스레드)가 차단됨을 의미한다.
스레드는 값비싼 리소스이고 스레드를 차단하는 것은 비효율적이기 때문에 runBlocking
은 응용 프로그램의 top-level에서는 잘 사용되지 않는다.
구조적 동시성
코루틴은 구조적 동시성의 원칙을 따른다. 즉, 새로운 코루틴은 코루틴의 수명을 제한하는 특정 코루틴 스코프에서만 실행할 수 있다.
Extract function 리팩토링
위의 예시에서 launch { ... }
내부의 코드 블록을 별도의 함수로 추출해 보자. 이 코드에서 Extract function 리팩토링을 진행하면 suspend 수식어가 붙은 새 함수를 얻을 수 있다. suspend 함수는 일반 함수와 마찬가지로 코루틴 내부에서 사용할 수 있지만 다른 suspend 함수(이 예제의 경우 delay()
)를 사용하여 코루틴을 일시 중지 할 수 있다는 추가 기능이 있다.
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
Scope builder
다른 코루틴 빌더에서 제공하는 코루틴 스코프 외에도 coroutineScope
빌더를 사용하여 고유한 스코프를 선언할 수 있다. coroutineScope
빌더는 코루틴 스코프를 만들고 실행된 모든 자식(코루틴)이 완료될 때까지 완료되지 않는다.
runBlocking
과 coroutineScope
빌더 모두 본체와 자식이 완료될 때까지 기다리기 때문에 비슷해 보이지만 runBlocking은 대기를 위해 현재 스레드를 차단(block)하는 반면 coroutine은 현재 스레드를 일시 중지(suspend)하고 다른 용도를 위해 기본 스레드를 해제한다.
coroutineScope 빌더는 모든 suspend 함수에서 사용할 수 있다.
예시)
fun main() = runBlocking {
doWorld()
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
Scope builder와 동시성
하나의 coroutineScope 빌더로 suspend 험수 내의 여러 동시 작업을 수행할 수 있다.
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
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
위의 예시에서 두 개의 launch { ... }
블록은 동시에 실행된다.
“World 1”이 시작한지 1초 후 먼저 출력되고 “World 2”가 시작한지 2초 후 출력된다. coroutineScope는 두 개의 블록이 모두 완료된 후에만 완료되므로 doWorld()
완료 후 “Done”이 출력된다.
명시적 작업
launch
코루틴 빌더는 실행된 코루틴에 대한 헨들인 job 객체를 반환하고 명시적으로 완료를 기다릴 수 있다.
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
참고 문헌