- 코루틴은 비동기와 병렬성을 순차적으로 프로그래밍할 수 있는 방법을 제공한다.
- 플로우는 비동기와 병렬성을 스트림 형태로 풀 수 있도록 한다.
- 코틀린에서 코루틴을 생성하고 시작하는 데 사용되는 함수
- 코루틴의 결과나 상태관리를 담당
- 주요 빌더: launch, async, runBlocking, withContext
- runBlocking 외 코루틴 빌더는 반드시 다른 코루틴 빌더 내에서 실행해야 한다.
fun main(): Unit = runBlocking<Unit> {
println(Thread.currentThread().name)
println(this)
}output
main @coroutine#1
"coroutine#1": BlockingCoroutine{Active}@2471cca7
- 자바에서 this를 출력하면 현재 오브젝트의 클래스 이름과 JVM 메모리 주소를 출력하는 것과 마찬가지로, 코루틴 안에서 this를 출력할 경우 BlockingCoroutine이 출력됨을 볼 수 있다. BlockingCoroutine 객체는 CoroutineScope의 자식이며 runBlocking을 사용할 경우 쓰레드 풀에서 다른 쓰레드를 사용하지 않고 현재 main 쓰레드를 사용한다.
- runBlocking 코루틴 빌더는 현재 쓰레드를 block 하고 코루틴을 실행한다. 물론 해당 코루틴을 현재 쓰레드가 실행할 수도 잇다.
- runBlocking 코루틴 빌더는 반환값을 지정할 수 있으며 반환값이 없는 경우 보통 이 생략된다. (컴파일러가 알 수 있는 경우 생략 가능)
- 모든 코루틴은 CoroutineScope로 시작한다.
- 즉, 위 코드에서 runBlocking과 같은 방식으로 코루틴을 정의한 구간에서는 CoroutineScope가 갖는 필드를 참조할 수 있다.
- CoroutineScope는 코루틴에 대한 정보인 coroutineContext를 가진다.
fun main() = runBlocking {
println(coroutineContext)
println(Thread.currentThread().name)
println(this)
}output
[CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@67117f44, BlockingEventLoop@5d3411d]
main @coroutine#1
"coroutine#1":BlockingCoroutine{Active}@67117f44
- runBlocking 코루틴 빌더가 block 되어 실행되었다면 launch 코루틴 빌더는 새로운 코루틴을 생성해서 실행된다.
- launch 코루틴 빌더는 가능하다면 병렬적으로 코루틴이 실행되도록 한다.
- runBlocking, coroutineScope 빌더 안에만 선언될 수 있다.
fun main() = runBlocking {
runBlocking {
println(coroutineContext)
println("runBlocking1: ${Thread.currentThread().name}")
println(this)
}
launch {
println(coroutineContext)
println("launch1: ${Thread.currentThread().name}")
println(this)
}
println(coroutineContext)
println("runBlocking2: @{Thread.currentThread().name}")
println(this)
}output
[CoroutineId(2), "coroutine#2":BlockingCoroutine{Active}@5d3411d, BlockingEventLoop@2471cca7]
runBlocking1: main @coroutine#2
"coroutine#2":BlockingCoroutine{Active}@5d3411d
[CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@483bf400, BlockingEventLoop@2471cca7]
runBlocking2: main @coroutine#1
"coroutine#1":BlockingCoroutine{Active}@483bf400
[CoroutineId(3), "coroutine#3":StandaloneCoroutine{Active}@33a10788, BlockingEventLoop@2471cca7]
launch1: main @coroutine#3
"coroutine#3":StandaloneCoroutine{Active}@33a10788
- 위와 같이 launch는 main 블록이 먼저 실행된 후에 실행되도록 설계되어 있다.
- 또한 코루틴 빌더에 디스패처를 등록하지 않았기 때문에 Default 디스패처가 사용되어 모두 같은 쓰레드에서 실행되었다.
- suspension point(중단점) 지정을 통한 코루틴 간 비동기 처리
- delay 함수는 코루틴이나 suspend 함수에서만 호출 가능
fun main() {
println("[0] main: ${Thread.currentThread().name}")
runBlocking {
launch {
println("[2] launch1: ${Thread.currentThread().name}")
delay(1000L)
println("[5] launch1 end")
}
launch {
println("[3] launch2: ${Thread.currentThread().name}")
}
println("[1] runBlocking: ${Thread.currentThread().name}")
delay(500L)
println("[4] runBlocking end")
}
println("[6] main end")
}output
[0] main: main
[1] runBlocking: main @coroutine#1
[2] launch1: main @coroutine#2
[3] launch2: main @coroutine#3
[4] runBlocking end
[5] launch1 end
[6] main end
- delay 함수는 코루틴이 사용 중인 쓰레드를 잠시 놓아주고 다른 코루틴이 사용할 수 있도록 한다(non-blocking)
- sleep은 현재 쓰레드를 잡아둔 채로 실행을 중지하지만 delay는 쓰레드를 놓아주기 때문에 비동기 작업 설계 시 사용될 수 있다.
- 다른 언어에서 코루틴은 계층적이지 않은 경우가 많은데, 코틀린에서의 코루틴들은 계층적인 구조를 가진다. 따라서 부모 코루틴이 취소되면 자식 코루틴도 모두 취소된다. 또한 Orphan 코루틴이 생기지 않도록 부모 코루틴은 항상 자식 코루틴이 끝나기를 기다린다. ([4], [5] 출력 결과 참고)
- suspend 함수는 중단 가능한 함수이다.
- 따라서 suspend 함수는 다른 코루틴이나 suspend 함수를 호출할 수 있다.
- suspend 함수는 코루틴이 아니다. 잠시 중단될 수 있는 함수임을 정의한 것뿐이다.
suspend fun do1() {
println("[2] launch1: ${Thread.currentThread().name}")
delay(1000L)
println("[5] launch1 end")
}
fun do2() {
println("[3] launch2: ${Thread.currentThread().name}")
}
suspend fun do3() {
println("[1] runBlocking: ${Thread.currentThread().name}")
delay(500L)
println("[4] runBlocking end")
}
fun main() {
println("[0] main: ${Thread.currentThread().name}")
runBlocking<Unit> {
launch {
do1()
}
launch {
do2()
}
do3()
}
println("[6] main end")
}output
[0] main: main
[1] runBlocking: main @coroutine#1
[2] launch1: main @coroutine#2
[3] launch2: main @coroutine#3
[4] runBlocking end
[5] launch1 end
[6] main end
*코드 실행 흐름 분석
- [0]main: 가장 먼저 실행된다.
- runBlocking: 현재 스레드(main)를 점유하고 내부 코드를 실행한다.
- launch (do1): 코루틴을 예약(큐에 삽입)만 하고 즉시 실행하지 않는다. 다음 줄로 넘어간다.
- launch (do2): 역시 코루틴을 예약만 하고 다음 줄로 넘어간다.
- [1]do3 시작: runBlocking 본문에 있는 do3()가 직접 호출된다.
- 여기서 delay(500L)을 만난다.
- delay는 스레드를 차단하지 않고 제어권을 양보 한다.
- 이제 runBlocking이 점유하던 스레드가 비었으므로, 대기 중이던 첫 번째 launch(do1)가 실행된다.
- [2]do1 시작: do1이 실행되다가 delay(1000L)을 만난다. 다시 제어권을 양보한다.
- [3]do2 시작: 대기열에 있던 두 번째 launch(do2)가 실행된다. do2는 일반 함수이므로 바로 끝난다.
- 시간 경과 및 재개:
- 500ms 후: do3의 지연이 먼저 끝난다. 제어권을 다시 가져와 [4]do3 종료를 출력한다.
- 1000ms 후: do1의 지연이 끝난다. 제어권을 가져와 [5]do1 종료를 출력한다.
- runBlocking 종료: 모든 자식 코루틴이 완료되었으므로 runBlocking이 끝난다.
- [6]main end: 마지막 문장이 실행된다.
// Error Code
suspend fun example() {
launch { // Unresolved reference: launch
println("${Thread.currentThread().name}")
delay(500L)
}
}
fun main(): Unit = runBlocking<Unit> {
example()
}- 위 코드는 컴파일 에러가 발생하는 코드이다. runBlocking을 제외한 모든 코루틴은 다른 코루틴 안에서 실행되어야 하기 때문이다. 따라서 launch 빌더로 코루틴을 생성할 수 없게 된다.
위와 같은 상황에서 코루틴을 생성하기 위해 코루틴 스코프 빌더가 적용된다.
suspend fun example() = coroutineScope {
this.launch {
println("${Thread.currentThread().name}")
delay(500L)
}
}
fun main(): Unit = runBlocking<Unit> {
example()
}즉, suspend 함수와 같이 코루틴이 정의될 수 있는 함수에 코루틴 빌더를 사용하고 싶은 경우 coroutineScope를 사용하면 된다. coroutineScope 빌더 역시 하나의 코루틴을 생성하기 때문에 내부에 선언된 모든 코루틴이 끝난 뒤에 coroutineScope로 선언한 코루틴 빌더가 모두 완료하게 된다.
runBlocking 코루틴 빌더는 현재 쓰레드를 block 하고 코루틴을 실행한다면 coroutineScope 빌더는 launch와 비슷하게 현재 쓰레드가 다른 일을 할 수 있도록 설계되어 있다. (사실 runBlocking, withContext 코루틴 빌더 외 대부분의 코루틴 빌더는 모두 non-block이다.)
- launch와 같이 비동기로 실행된 코루틴에 대한 상태 정보들은 Job 객체로 참조하여 각종 조작을 할 수 있다.
- 아래는 Job 객체에 대해 suspension point를 지정한 join 함수이다.
- join 함수는 해당 코루틴이 끝나기를 기다린다.
suspend fun example() = coroutineScope {
var job = this.launch {
println("[1] ${Thread.currentThread().name}")
delay(500L)
}
job.join()
this.launch {
println("[3] ${Thread.currentThread().name}")
delay(500L)
}
println("[2]")
}
fun main(): Unit = runBlocking<Unit> {
example()
println("[4]")
}output
1
2
3
4
- 예제
fun main() = runBlocking {
println("[A]")
val job = launch {
println("[B]")
delay(1000L)
println("[C]")
}
launch {
println("[D]")
delay(500L)
println("[E]")
}
println("[F]")
job.join()
println("[G]")
}답:
A
F
B
D
E
C
G
- 예제
fun main() = runBlocking {
println("[1]")
launch {
delay(200L)
println("[2]")
}
coroutineScope {
launch {
delay(500L)
println("[3]")
}
println("[4]")
}
println("[5]")
}답:
1
2
3
4
5
- launch 코루틴 빌더와 마찬가지로 새로운 코루틴을 만들고 실행한다.
- launch는 Job을 통해 수행결과를 받아올 수 있지만 async는 await 키워드를 통해 수행 결과를 받아올 수 있다.
- 코루틴 결과를 받아야 할 경우 async, 결과를 받지 않을 경우 launch를 사용한다.
suspend fun example(): Int {
delay(100L)
return Random.nextInt(0, 10)
}
fun main() = runBlocking {
var res = async { example() } // this: 코루틴
println("${res.await()}") // job의 join + return 값과 같음
}- await이 호출되는 지점에선 실행 중단을 잠시 멈추고 결과가 반환되기를 기다리는 suspension point이다.
- async 키워드는 작업 내용을 큐에 넣는 행위로 실제 실행 시점은 알 수 없다.
- 코루틴은 서로 계층 구조로 구성되기 때문에 코루틴에서 예외가 발생하면 부모 및 형제 코루틴에게도 모두 전파된다.
- 전파를 받은 부모 및 형제 코루틴들은 모두 작업을 cancel 한다.
suspend fun example1(): Int {
try {
delay(100L)
return Random.nextInt(0, 10)
} finally {
println(">>> example1 canceled")
}
}
suspend fun example2(): Int {
try {
delay(100L)
throw RuntimeException()
} catch (e: Exception) {
println(">>> example2 canceled")
throw e
}
}
suspend fun exampleParent() = coroutineScope {
var res1 = async { example1() }
var res2 = async { example() }
try {
println("${res1.await() + res2.await()}")
} catch (e: Exception) {
println(">>> exampleParent canceled")
}
}
fun main() = runBlocking {
try {
exampleParent()
} catch (e: Exception) {
println("catch in main")
}
}output
>>> example1 canceld
>>> example2 canceld
>>> exampleParent canceled
catch in main
- async 키워드는 작업 내용을 큐에 넣는 행위이기 때문에 예외는 await에서 받아올 수 있다.
- 위와 같이 하나의 all-or-nothing 성향의 작업을 여러 코루틴으로 나누고 작업한다면 특정 코루틴에서 실패시 전체 코루틴이 취소되도록 할 수 있다.
- 코루틴 블록 안에서는 CoroutineScope라는 객체로 정의된다.
- 코루틴 블록 안에서의 this는 CoroutineScope 객체이다.
- CoroutineScope가 가지는 필드 중 코루틴에 대한 정보인 coroutineContext가 있다.
- 코루틴을 만드는 방법
- 코루틴 빌더 사용: launch, async, runBlocking, withContext
- 코루틴 스코프 빌더 사용: coroutineScope(suspend 함수 내에서 코루틴 정의 시)
- 현재 쓰레드를 block 하고 코루틴을 실행하는 빌더: runBlocking, withContext
- 코틀린에서의 코루틴들은 계층적인 구조로 구성
- 코틀린 코루틴의 부모 코루틴이 취소되면 자식 코루틴도 모두 취소됨
- Orphan 코루틴이 생기지 않도록 부모 코루틴은 항상 자식 코루틴이 끝나기를 기다림
- suspend 함수: delay 등으로 중단될 수 있는 코루틴이 정의된 함수
- 코루틴 결과를 받아야 할 경우 async, 결과를 받지 않을 경우 launch를 사용
- 코루틴은 계층 구조를 가지며 특정 코루틴에서 예외 발생시 부모 형제 코루틴 역시 cancel 된다.