코루틴(Coroutines)
코루틴이란?
비선점형 (협력형) 멀티 태스킹 (non-preemptive multitasking)으로 실행을 일시 중단(suspend) 하고 재개(resume) 할 수 있는 여러 진입 지점을 허용한다.
서로 협력해서 실행을 주고받으면서 작동하는 여러 서브루틴을 말한다.
일반적인 서브루틴은 오직 한 가지 진입 지점만을 가진다. 함수를 호출하는 부분이며, 그때마다 활성 레코드 (activation record)가 스택에 할당되면서 서브루틴 내부의 로컬 변수등이 초기화 된다. 또한 서브루틴에서 반환되고 나면 활성 레코드가 스택에서 사라지기 떄문에 모든 상태를 잃어버린다.
1 | fun main() { |
- 반면 코루틴은 실행을 일시 중단하고 진입 지점을 허용한다.
1 | fun log(msg:String , self : Any?) = println("Current Thread : ${Thread.currentThread().name} / this : $self :$msg") |
실행로그
1 | Current Thread : main / this : null :main routine started |
위 코드를 분석하기 전에 각각 함수가 하는 역할을 정리하면 다음과 같다.
- runBlocking : coroutine builder 로서 내부 코르틴이 모두 끝난 다음에 반환된다.
- launch : coroutine builder로서 , 넘겨받은 코드 블록으로 새로운 코르틴을 생성하고 실행시켜준다.
- yield : 해당 코르틴이 실행권을 양보하고, 실행 위치를 기억하고, 다음 호출때는 해당 위치부터 다시 실행한다.
위 코르틴에서 1,3,5를 출력하는 코르틴과 2,4,6를 출력하는 코르틴이 서로 실행권을 양보해가면서 실행된다. 한가지 유의할점은 마치 병렬적으로 실행되는 것처럼 보이지만 다른 쓰레드가 아니라 하나의 쓰레드에서 수행된다는 점이다. 따라서 Context Switching 도 발생하지 않는다.
- Launch coroutine Builder는 Job 객체를 반환한다. Job은 N개 이상의 coroutines의 동작을 제어할 수도 있으며, 하나의 coroutines 동작을 제어할수도 있다.
1 | suspend fun main() = coroutineScope { |
Job
코루틴의 Job 객체는 코루틴의 상태를 가지고 있다.
1 | wait children |
- start : job을 실행하고, 호출시 코루틴이 동작중이면 true, 준비 및 완료 상태면 false를 반환된다.
- join : job을 실행하고, Job의 동작이 완료될떄까지 job을 호출한 코루틴을 일시중단한다.
- cancel : 현재 코루틴에 종료를 유도하고, 대기하지 않는다.
- cancelAndJoin : 현재 코루틴을 즉시 종료하라는 신호를 보낸후 대기한다.
- cancelChildren : 하위 자식 코루틴을 종료시킨다.
Async
Async는 사실상 Launch와 같은일을 수행하는데, 차이점은 Launch는 Job객체를 반환하는 반면 , Async는 Deffered를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13public interface Deferred<out T> : Job {
public suspend fun await(): T
public val onAwait: SelectClause1<T>
@ExperimentalCoroutinesApi
public fun getCompleted(): T
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?
}Deffered는 Job을 상속한 클래스로서, 타입 파라미터가 있는 제너릭 타입이며, Job과 다르게 await 함수가 정의되어 있다.
Deffered의 타입 파라미터는 Deffered 코루틴이 계산 후 돌려주는 값의 타입이다. 즉 Job은 Deffered<Unit>라고 생각할수도 있다.
정리하면 async는 코드 블록을 비동기로 실행 할 수 있고, async가 반환하는 Deffered의 await를 사용해서 코루틴이 결과 값을 내놓을떄까지 기다렸다가 결과값을 받아올 수 있다.
이떄 비동기로 실행할떄 제공되는 코루틴 컨텍스트에 따라 하나의 Thread안에서 제어만 왔다 갔다 할수도 있고, 여러 Thread를 사용할 수도 있다.
1 | fun sumAll(){ |
실행로그를 보면 다음과 같다.
1 | after d1 |
만약 위 코드를 직렬화해서 실행하면 최소 6초의 시간이 걸리겠지만, async로 비동기적으로 실행하면 3초가량이 걸리며 더군다나 위 코드는 별개의 thread가 아니라 main thread 단일 thread로 실행되어 이와 같은 성능상 이점을 얻을수 있다.
특히 이와 같은 상황에서 코루틴이 장점을 가지는 부분은 I/O로 인한 장시간 대기
, CPU 코어수가 작아 동시에 병렬적으로 실행 가능한 쓰레드 개수 한정된 상황
이다.
코루틴 컨텍스트
- Launch , Async 등은 모두 CoroutineScope의 확장함수로 실제로 CoroutineScope는 CoroutineContext 필드를 이런 확장함수 내부에서 사용하기 위한 매개체 역할을 수행한다. 원한다면 launch,aync 확장함수에 CoroutineContext를 넘길수도 있다.
1 |
|
그렇다면 CoroutineContext가 하는 역할은 무엇일까?
- 코루틴이 실행중인 여러 작업과 디스패처를 저장하는 일종의 맵으로 이 CoroutineContext를 사용해 다음에 실행할 작업을 선정하고, 어떻게 Thread에 배정할지에 대한 방법을 결정한다.
1 | fun context(){ |
실행로그를 보면 같은 launch 확장함수를 사용한다고 하더라도 실행되는 CoroutineContext에 따라 다른 Thread상에서 코루틴이 실행됨을 확인할 수 있다.
1 | use main thread :main |
Coroutine Builder 와 Suspending Function
앞선 Launch , Async , runBlocking , CoroutineScope모두 코루틴 빌더라고 , 새로운 코루틴을 만들어주는 함수이다.
delay , yield 함수는 일시중단 함수로 이외에도 다른 일시중단 함수들이 존재한다.
- withContext: 다른 컨텍스트로 코루틴 전환
- withTimeOut : 일정 시간내 코루틴이 실행되지 않으면 예외 발생
- withTimeOutOrNull : 일정 시간내 코루틴이 실행되지 않으면 null 반환
- awaitAll : 모든 작업의 성공을 대기 , 만약 하나라도 예외 발생시 실패처리
- joinAll : 모든 작업이 종료될떄까지 현재 작업 대기