# 코루틴과 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