# 코루틴과 Async/Await
- [Kotlin Docs](<https://kotlinlang.org/docs/coroutines-guide.html>)
- 코루틴
- 비선점형 멀티태스킹을 수행하는 일반화한 서브루틴이다.
- 코루틴은 실행을 일시 중단(suspend)하고 재개(resume)할 수 있는 여러 진입 지점(entry point)을 허용
- 서브루틴 : 반복 호출할 수 있게 정의한 프로그램 구성 요소(함수라고도 말한다.)
- 멀티태스킹: 여러 작업을 동시에 수행하는 것처럼 보이거나 실제로 동시에 수행하는 것
- 비선점형: 멀티태스킹의 각 작업을 수행하는 참여자들의 실행을 운영체제가 강제로 일시 중단시키고 다른 참여자를 실행하게 만들 수 없다는 뜻.
- 따라서 각 참여자들이 서로 자발적으로 협력해야만 비선점형 멀티태스킹이 제대로 작동할 수 있다.
- 코틀린의 코루틴 지원: 일반적인 코틀린
- 코틀린의 코루틴 지원 기본 기능들은 `kotlin.coroutine` 패키지 밑에 있다.
- 코틀린이 지원하는 기본 기능을 활용해 다양한 형태의 코루틴은 `kotlinx.coroutines` 패키지 밑에 있다.
- [kotlinx.coroutines](<https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md#using-in-your-projects>)
### kotlinx.coroutines
- 코루틴을 만들어주는 코루틴 빌더(coroutine builder)
- 코루틴 빌더에 원하는 동작을 람다로 넘겨서 코루틴을 만들어 실행하는 방식으로 코루틴을 활용
### kotlinx.coroutines.CoroutineScope.launch
```kotlin
package coroutine
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
fun now() = LocalDateTime.now().toLocalTime().truncatedTo(ChronoUnit.MILLIS)
fun log(msg: String) = println("${now()} : ${Thread.currentThread()}: ${msg}")
fun launchInGlobalScope() {
GlobalScope.launch {
log("coroutine started")
}
}
fun main(args: Array<String>) {
log("main() started")
launchInGlobalScope()
log("launchInGlobalScope() executed")
Thread.sleep(5000L)
log("main() terminated")
}
/*
* 15:54:51.569 : Thread[main,5,main]: main() started
* 15:54:51.691 : Thread[main,5,main]: launchInGlobalScope() executed
* 15:54:51.700 : Thread[DefaultDispatcher-worker-1,5,main]: coroutine started
* 15:54:56.699 : Thread[main,5,main]: main() terminated
* */
launch는 코루틴을 잡으로 반환하며, 만들어진 코루틴은 기본적으로 즉시 실행된다.
- GlobalScope.launch가 만들어낸 코루틴이 서로 다른 스레드에서 실행된다. GlobalScope는 메인 스레드가 실행 중인 동안만 코루틴의 동작을 보장해준다. 그래서 GlobalScope()를 사용할
때는 조심해야한다
- 코루틴이 동작하기 전에 메인 스레드가 종료되면 바로 프로그램 전체가 끝남.
runBlocking
- 이를 방지하기 위해 비동기적으로 launch를 실행하거나, launch가 모두 다 실행될 때까지 기다려야 함.
- 코루틴이 끝날 때까지 현재 스레드를 블록시키는 함수
runBlocking()이 있다.
runBlocking은 CoroutineScope의 확장 함수가 아닌 일반 함수이기 때문에 별도의 코루틴 스코프 객체 없이 사용 가능
package coroutine
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
fun now() = LocalDateTime.now().toLocalTime().truncatedTo(ChronoUnit.MILLIS)
fun log(msg: String) = println("${now()} : ${Thread.currentThread()}: ${msg}")
fun runBlockingExample() {
runBlocking {
launch {
log("coroutine started")
}
}
}
fun main(args: Array<String>) {
log("main() started")
runBlockingExample()
log("runBlockingExample() executed")
log("main() terminated")
}
/*
* 16:18:28.613 : Thread[main,5,main]: main() started
* 16:18:28.708 : Thread[main,5,main]: coroutine started
* 16:18:28.710 : Thread[main,5,main]: runBlockingExample() executed
* 16:18:33.715 : Thread[main,5,main]: main() terminated
* */
yield
package coroutine
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
fun log(msg: String) = println("${now()} : ${Thread.currentThread()}: ${msg}")
fun yieldExample() {
runBlocking {
launch {
log("1")
yield()
log("3")
yield()
log("5")
}
log("after first launch")
launch {
log("2")
delay(1000L)
log("4")
delay(1000L)
log("6")
}
log("after second launch")
}
}
fun main(args: Array<String>) {
log("main() started")
yieldExample()
log("after runBlocking")
log("yieldExample() executed")
log("main() terminated")
}
/*
* 16:29:25.512 : Thread[main,5,main]: main() started
* 16:29:25.622 : Thread[main,5,main]: after first launch
* 16:29:25.629 : Thread[main,5,main]: after second launch
* 16:29:25.632 : Thread[main,5,main]: 1
* 16:29:25.633 : Thread[main,5,main]: 2
* 16:29:25.644 : Thread[main,5,main]: 3
* 16:29:25.644 : Thread[main,5,main]: 5
* 16:29:26.646 : Thread[main,5,main]: 4
* 16:29:27.651 : Thread[main,5,main]: 6
* 16:29:27.655 : Thread[main,5,main]: after runBlocking
* 16:29:27.655 : Thread[main,5,main]: yieldExample() executed
* 16:29:27.655 : Thread[main,5,main]: main() terminated
* */
- launch는 즉시 반환된다
- runBlocking은 내부 코루틴이 모두 끝난 다음에 반환된다.
- delay()를 사용한 코루틴은 그 시간이 지날 때 까지 다른 코루틴에게 실행을 양보한다.
- 앞 코드에서 delay(1000L) 대신 yield()를 쓰면 차례대로 1, 2, 3, 4, 5, 6이 표시될 것이다.
- 첫 번째 코루틴이 두 번이나 yield()를 했지만 두 번째 코루틴이 delay() 상태에 있었기 때문에 다시 제어가 첫 번째 코루틴에게 돌아왔다.
kotlin.coroutines.CoroutineScope.async
async는 launch와 같은 일을 함.
- 차이: launch는 Job을 반환, async는 Deffered를 반환
- Deffered는 Job을 상속한 클래스이기 때문에 launch 대신 async를 사용해도 아무 문제가 없다.
- Deffered와 Job의 차이는, Job은 아무 타입 파라미터가 없는데, Deffered는 타입 파라미터가 있는 제네릭 타입이라는 점과 Deffered 안에는 await()함수가 정의돼 있다는
점이다.
- Job은 Unit을 돌려주는 Deffered<Unit>이라고 생각할 수도 있다.
- async는 코드 블록을 비동기로 실행할 수 있다
- 제공하는 코루틴 컨텍스트에 따라 여러 스레드를 사용하거나 한 스레드 안에서 제어만 왔다 갔다 할 수도 있다.
- async가 반환하는 Deffered의 await을 사용해서 코루틴이 결과 값을 내놓을 때까지 기다렸다가 결과 값을 얻어낼 수 있다.
fun sumAll() {
runBlocking {
val d1 = async { delay(1000L); 1 }
log("after async(d1)")
val d2 = async { delay(2000L); 2 }
log("after async(d2)")
val d3 = async { delay(3000L); 3 }
log("after async(d3)")
log("1+2+3 = ${d1.await() + d2.await() + d3.await()}")
log("after await all & add")
}
}
fun main() {
sumAll()
}
/*
* 17:09:01.850 : Thread[main,5,main]: after async(d1)
* 17:09:01.864 : Thread[main,5,main]: after async(d2)
* 17:09:01.867 : Thread[main,5,main]: after async(d3)
* 17:09:02.888 : Thread[main,5,main]: 1+2+3 = 6
* 17:09:02.889 : Thread[main,5,main]: after await all & add
* */
- d1, d2, d3 를 하나로 순서대로(병렬 처리에서 이런 경우를 직렬화해 실행한다고 말한다.) 실행하면 총 6초(6000 밀리초) 이상이 걸려야 하지만, 6이라는 결과를 얻을 떄까지 총 3초가 걸렸음을 알 수
있다.
코루틴 컨텍스트와 디스패처
launch, async 등은 모두 CoroutineScope의 확장 함수다.
CoroutineScope 에는 CoroutineContext 타입의 필드 하나만 들어있다
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineContext 실제로 코루틴이 실행 중인 여러 작업(Job 타입)과 디스패처를 저장하는 일종의 맵
- 코틀린 런타임은 이
CoroutineContext 를 사용해 다음에 실행할 작업을 선정하고, 어떻게 스레드에 배정할지 대한 방법을 결정한다.
package coroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.runBlocking
fun dispatcherExample() {
runBlocking {
launch { // 부모 컨텍스트를 사용 이경우(main)
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // 특정 스레드에 종속되지 않음 ? 메인 스레드 사용
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // 기본 디스패처를 사용
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 새 스레드 사용
println("newSingleThreadContext : I'm working in thread ${Thread.currentThread().name}")
}
}
}
fun main(args: Array<String>) {
dispatcherExample()
}
/*
* Unconfined : I'm working in thread main
* Default : I'm working in thread DefaultDispatcher-worker-2
* main runBlocking : I'm working in thread main
* newSingleThreadContext : I'm working in thread MyOwnThread
* */
코루틴 빌더와 일시 중단 함수
- 코루틴 빌더
- launch, async, runblocking은 모두 코루틴 빌더
- produce
- 정해진 채널로 데이터를 스트림으로 보내는 코루틴을 만든다. 이 함수는 ReceiveChannel<>을 반환한다. 그 채널로부터 메시지를 전달받아 사용할 수 있다
- actor
- 정해진 채널로 메시지를 받아 처리하는 액터를 코루틴으로 만든다. 이 함수가 반환하는 sendChannel<> 채널의 send() 메서드를 통해 액터에게 메시지를 보낼 수 있다.
- 일시 중단 함수
- delay(), yield()
- withContext
- withTimeout
- 코루틴이 정해진 시간 안에 실행되지 않으면 예외를 발생시키게 한다
- withTimeoutOrNull
- 코루틴이 정해진 시간 안에 실행되지 않으면 null을 결과로 돌려준다
- awaitAll
- 모든 작업의 성공을 기다린다. 작업 중 어느 하나가 예외로 실패하면 awaitAll도 그 예외로 실패한다
- joinAll
- 모든 작업이 끝날 떄까지 현재 작업을 일시 중단시킨다
suspend
package coroutine
import kotlinx.coroutines.*
suspend fun yieldThreeTimes() {
log("1")
delay(1000L)
yield()
log("2")
delay(1000L)
yield()
log("3")
delay(1000L)
yield()
log("4")
}
fun main(args: Array<String>) {
runBlocking {
launch { yieldThreeTimes() }
}
}
- yield()를 해야 하는 경우 동작 방식
- 코루틴에 진입할 때 코루틴에서 나갈 때 코루틴이 실행 중이던 상태를 저장하고 복구하는 등의 작업을 할 수 있어야 한다.
- 현재 실행 중이던 위치를 저장하고 다시 코루틴이 재개될 때 해당 위치부터 실행을 재개할 수 있어야 한다
- 다음에 어떤 코루틴을 실행할지 결정한다
- 이 세 가지 중 동작은 코루틴 컨텍스트에 있는 디스패처에 의해 수행된다. 일시 중단 함수를 컴파일하는 컴파일러는 앞의 두 가지 작업을 할 수 있는 코드를 생성해내야 한다.
- 이때 코틀린은 컨티뉴에이션 패싱 스타일(CPS, continuation passing style) 변환과 상태 기계(state machine)를 활용해 코드를 생성해낸다.
제네레이터 빌더
kotlin-example