정의
코루틴(coroutine)은 co(cooperation) + routine의 합성어이다. 멜빈 콘웨이가 1958년에 만들어 냈으며 당시 어셈블리 프로그램에 적용했다. 코루틴은 협동루틴, 상호 연계 프로그램이라고 표현할 수 있다. 루틴과 서브 루틴은 서로 비대칭적인 관계이지만, 코루인은 완전히 대칭적인, 서로가 서로를 호출하는 관계이다.
코틀린 공식문서에선 다음과 같이 말한다.
코루틴은 일시 중단 가능한 계산 인스턴스입니다. 코드의 나머지 부분과 동시에 작동하는 코드 블록을 실행한다는 점에서 개념적으로 스레드와 유사합니다. 그러나 코루틴은 특정 스레드에 바인딩 되지 않습니다. 한 스레드에서 실행을 일시 중지하고 다른 스레드에서 다시 시작할 수 있습니다.
코루틴을 경량 스레드로 생각할 수 있지만 실제 사용해보면 스레드와 매우 다르게 만드는 중요한 차이점이 많이 있습니다.
첫 번째 코루틴
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { // 새 코루틴을 시작
delay(1000L) // 논 블록으로 1초간 지연(ms단위)
println("World!") // 지연 이후 출력
}
println("Hello") // 메인 코루틴은 이전 코루틴이 지연되는 동안 계속 진행
}
-result-
Hello
World!
위 코드가 무슨 코드인지 분석해보자.
- launch : 코루틴 빌더이다. 독립적으로 계속 작동하는 나머지 코드와 동시에 새 코루틴을 시작한다. 그래서 Hello가 먼저 출력됐다.
- delay : 지연은 논-블록 일시중단 기능이다. 특정 시간동안 코루틴을 일시중단한다. sleep과 다르게 기본 스레드가 블록되지 않지만 다른 코루틴이 실행되고 코드에 기본 스레드를 사용할 수 있다.
- runBlocking : fun main()안의 코루틴 코드와 코루틴이 아닌 외부를 연결하는 코루틴 빌더이다. 만약 runBlocking를 제거하면 에러를 볼 수 있다. 왜냐면 launch는 코루틴 스코프 안에서만 정의되기 때문이다.
Unresolved reference: launch
runBlocking의 의미는 사용하고 있는 스레드(위 코드의 경우 메인 스레드)를 내부의 모든 코루틴이 실행을 완료할 때 까지 블록하는 것을 의미한다. 스레드는 비싼 리소스이고 스레드를 블록하는 것은 비효율적이므로 응용 프로그램 최상위 수준에서 사용되는 경우는 거의 없다.
구조적 동시성
코루틴은 구조화된 동시성의 원칙(structured concurrency)을 따른다. 새로운 코루틴은 코루틴의 수명을 제한하는 특정 CoroutineScope에서만 시작될 수 있다. 위 예제는 runBlocking이 해당 범위를 설정하고 World!를 1초 지연후 인쇄될 때까지 블록됬다가 종료된다는 것을 보여준다.
실제 애플리케이션 에서는 많은 코루틴을 시작하게 된다. 스코프 밖은 하위 코루틴이 끝날 때 까지 완료할 수 없다. 구조적 동시성은 손실과 누수되지 않도록 한다. 또한 코드의 모든 오류가 적절하게 보고되고 손실되지 않도록 한다.
추출 기능 리팩토링
launch{...} 내부 코드블록을 별도의 함수로 추출해보자. 함수를 추출하게 되면 suspend 식별자가 있는 새 함수를 얻는다.
이 함수는 첫 번째 서스펜딩 함수이다. 서스펜딩 함수는 코루틴안에서 일반 함수처럼 쓰일 수 있고, 추가적으로 다른 서스펜딩 함수를 써서 코루틴의 실행을 일시중지 할 수 있다.
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// 서스펜딩 함수
suspend fun doWorld() {
delay(1000L)
println("World!")
}
-result-
Hello
World!
스코프 빌더
다른 빌더에서 제공하는 코루틴 스코프 외에도 coroutineScope를 사용하면 빌더를 직접 선언할 수 도 있다. 다른 빌더와 마찬가지로 코루틴 범위를 만들고 실행된 모든 자식이 완료될 때 까지 완료되지 않는다.
이전에 썼던 runBlocking과 coroutineScope는 둘다 자식이 완료될 때 까지 기다리기 때문에 비슷해 보일 수 있지만 runBlocking는 결과를 기다리기 위해 현재 스레드를 블로킹하는 반면 coroutineScope는 일시중지하여 다른 용도를 위해 기본 스레드를 블록킹하지 않는다는 것이다. 이 차이로 인해 runBlocking는 일반 함수이고 coroutineScope는 서스펜딩 함수이다. coroutineScope는 서스펜딩 함수 어디에서든 사용할 수 있다.
import kotlinx.coroutines.*
fun main = runBlocking{
doWorld()
}
suspend fun doWorld() = coroutineScope{
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
-result-
Hello
Whorld!
스코프 빌더와 동시성
coroutineScope빌더는 서스펜딩 함수 내에서 여러 작업을 동시에 수행하는 데 사용할 수 있다. doWorld 서스펜딩 함수에서 두 개의 코루틴을 동시에 실행해보자.
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
doWorld()
println("DONE")
}
suspend fun doWorld() = coroutineScope {
launch {
delay(2000L)
println("world2")
}
launch {
delay(1000L)
println("world1")
}
println("Hello")
}
-result-
Hello
World 1
World 2
Done
코드 안에 launch블록이 동시에 실행됬다. world1이 먼저 출력되고 2가 출력된다. 그리고 coroutineScope안에 있는 launch블록이 둘 다 끝나기 전까지 DONE는 출력되지 않고, 끝난 이후 출력됬다.
명시적 작업
lanuch코루틴 빌더는 실행된 코루틴에 대한 작업 객체를 반환하고 명시적으로 완료를 기다리는 데 사용할 수 있다. 예를 들면, 자식 코루틴이 완료될 때 까지 기다린 다음 "Done"문자열을 인쇄할 수 있다.
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val job = launch {
delay(1000L)
println("world")
}
println("hello")
job.join()
println("done")
}
-result-
hello
world
done
코루틴은 가볍다
마지막으로 코루틴은 JVM스레드보다 리소스 집약적이다. 스레드를 사용할 때 JVM의 가용 메모리를 소모하는 코드를 코루틴으로 작성하면 리소스 한계에 도달하지 않고 표현할 수 있다.
2000개 미만의 스레드에는 1.5GB 이상의 메모리가 필요하다. 100만 개의 코루틴은 700MB 미만의 메모리가 필요하다. 결론은 코루틴은 매우, 매우 가볍다는 것이다.
"함수형 코틀린" - 마리오 아리아스
출처
Coroutines basics | Kotlin
kotlinlang.org
'코틀린' 카테고리의 다른 글
기초구문, 이디엄(관용어) (0) | 2023.01.30 |
---|---|
스코프 함수 (0) | 2022.07.24 |
15. 인라인 함수 (0) | 2022.06.13 |
14. 고차함수와 람다 (0) | 2022.06.12 |
13. 함수 (0) | 2022.06.12 |