코루틴은 실행을 일시 중지할 수 있는, 스케쥴링 가능한 코드 블록 혹은 코드 블록들의 집합이다.
코루틴은 비동기 처리를 위한 기술로 스레드와 기능적으로 유사하지만,
스레드보다 가볍고 유연하며 여러 스레드 내에서 동일한 코루틴이 실행될 수 있다.
또한 코루틴은 Kotlin만 가지고 있는 기능이 아니다.
네이밍을 보면 "Ko"가 아닌 Co
(함께) + Routine
(Task) 이다.
위키피디아에서는 코루틴을 아래와 같이 말한다.
실행을 지연 및 재개함으로써 비선점적 멀티태스킹을 위한 서브 루틴을 일반화한 컴퓨터 프로그램 구성요소
main
함수에 의해 수행되는 흐름간단하게 Main 함수와 Main에서 호출하는 함수로 생각하면 된다.
서브 루틴의 경우 동기적으로 실행되기에 진입점과 탈출점이 명확하지만
코루틴의 경우 진입 후 임의 지점에서 실행 중 동작을 중단하고 이후 해당 지점에서부터 실행을 재개한다.
내부적으로 Continuation
dispatcher
등을 통해 Context 유지 및 적합한 쓰레드를 선택해 실행하게 된다.
비선점형 (coroutine)
하나의 프로세스가 CPU를 할당받으면 종료전까지 다른 프로세스가 CPU를 강제로 차지할 수 없다
논리적인 병렬 실행으로 시분할로 cpu를 나눠 사용해 병렬 실행처럼 보이게 만듦
선점형 (thread)
하나의 프로세스가 다른 프로세스 대신에 CPU를 강제로 차지할 수 있다.
물리적인 병렬 실행으로 실제로 동시에 작업을 진행
코루틴은 쓰레드가 아닌 쓰레드 내 동작하는 하나의 작업 단위이며 정의된 다양한 구성요소의 집합인 Context
를 오버라이드 하며 실행된다.
따라서 쓰레드 내 Context switching 없이 여러 코루틴을 실행, 중단, 재개하는 상호작용을 통해 동시성을 갖기에 쓰레드와 메모리 사용이 줄어든다.
코루틴은 Dispatcher에 의해 실행되는 환경(Thread)이 결정될 수 있지만, 그 자체로는 환경을 새로 구성하거나 변경하지 않는다.
만약 Dispatcher 를 재정의하지 않고 UI Dispatcher(Main Routine) 를 그대로 상속받아 사용한다면
일반적인 함수 호출(Sub Routine)과 동일하게 수행된다.
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(50_000) { // launch a lot of coroutines
launch {
delay(1000L)
print(".")
}
}
}
따라서 위의 코드가 OOM(Out-Of-Memory) 없이 동작할 수 있다
속해있는 코루틴 스코프에서 코루틴 생성(왼쪽 그림)
실행되는 함수들의 스레드는 Coroutine Scope가 갖는 Context의 디스페처에 의해 결정
생성시 Dispatcher를 설정할 경우 디스페처를 제외한 CoroutineContext를 전부 상속하여 생성
launch { }
와 빌더를 통해 코루틴으로 실행할 코드블록은 Continuation 이라는 단위로 생성
suspend(중단) 상태로 생성 되었다가 resume() 요청으로 인해 resumed 상태로 전환되어 내부 코드 로직 실행
resume이 요청될 때마다 현재 컨텍스트의 dispatcher 를 통해 적합한 실행 스레드를 통해 로직 실행
함수들의 연결을 Continuation 이라는 Callback을 두어 연결하는 방식으로 Continuation 단위로 dispatcher 를 변경 또는 실행 유예 등 플로우 컨트롤 용이
suspend
키워드는 일시 중단 함수로 정의하며, CoroutineScope 내에서만 실행가능
suspend fun helloWorld() {
delay(1000)
println("hello World")
}
현재 스레드에서 실행되는 코루틴을 만들고 코루틴이 완료될 때까지 현재 스레드의 실행을 Blocking 함
fun main(){
runBlocking {
delay(100)
println("finish")
}
}
CoroutineScope의 확장 함수로, 특정 코루틴 스코프에 코루틴 추가
fun main(){
CoroutineScope(Dispatchers.Default).launch {
delay(200)
println("finish")
}.join()
}
launch
와 유사하지만 await
을 통해 결과값 조회 가능
fun main(){
val result = async {
networkCall()
}
println(result.await())
}
suspend fun networkCall(): String {
delay(1000)
return "Answer 1"
}
fun main(){
launch(Dispatchers.Default) {
// TODO
}
}
fun main(){
launch(Dispatchers.IO) {
// TODO
}
}
fun main(){
launch(Dispatchers.Main) {
// TODO
}
}
아무 Context 를 지정하지 않고, 호출한 스레드에서 동작되게끔 한다
fun main(){
launch(Dispatchers.Unconfined) {
// TODO
}
}
코루틴 내부에서 오류가 발생했을 때 에러를 처리할 수 있는 CoroutineContext
fun main() = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler : $exception")
}
CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
throw IllegalArgumentException()
}.join()
}
지정된 suspend 블록 에서 cold flow 생성
cold flow
- 연산자가 결과에 적용될 때마다 블록이 호출된다는 것을 의미
- 단 한명의 구독자(collect 호출한 개체)만 존재하며 만약 새로운 구독자가 생길 경우 플로우를 다시 생성.
- 수집(coolect)은 생산자 코드(flow를 업데이트하는 코드)를 트리거합니다.
- collect 될 때마다 동작하지만, collect하는 소비자가 없으면 동작하지 않습니다.
fun fibonacci(): Flow<BigInteger> = flow {
var x = BigInteger.ZERO
var y = BigInteger.ONE
while (true) {
emit(x)
x = y.also {
y += x
}
}
}
fibonacci().take(100).collect { println(it) }
여러 개의 flow를 하나로 결합하는 연산
val flow = flowOf(1, 2, 3).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c", "d").onEach { delay(15) }
flow.zip(flow2) { i, s -> i.toString() + s }.collect {
println(it) // Will print "1a 2b 3c"
}