* Coroutine의 개념
코루틴은 1958년 멜빈 콘웨이가 처음 제창했다고 하는 개념으로 현재 여러 프로그래밍 언어에 구현되어 있으며 코틀리만의 고유한 개념은 아니다.
프로그래밍에서 루틴이란 프로그램의 흐름을 추상적으로 일컫는 말이다.
루틴은 크게 메인루틴과 서브루틴으로 나눌수있는데, 예를들어 main함수에 의해 수행되는 프로그램의 흐름을 메인루틴이라고 한다면, main함수 안에서 실행되는 개별 함수들에 의해 수행되는 흐름을 서브루틴이라 비유할 수 있다.
루틴의 한 종류인 함수는 호출을 함으로써 그 내부에 진입하여 내부에서 처리를 수행하고, 처리가 끝나면 함수도 종료되는 구조를 가지고 있기 때문에 보통은 루틴이 일직선적인 흐름을 가지고 있다고 할 수 있다.
그런데 코루틴의 특징은 이 일직선적인 흐름을 중간에 지연시켰다가(suspend) 다시 재시작(resume)이 가능하다. 따라서 코루틴을 사용하면 루틴이 실행 되었더라도 더 바쁜 다른 루틴이 실행되는 동안 잠시 멈췄다가, 바쁜게 끝나면 재시작하여 나머지 작업을 끝내는 형태의 비동기 프로그래밍이 가능하게 된다.
또 코루틴은 자기 스스로가 메인루틴이 되면서도 서브루틴이 될 수 있다는 특징도 있다.
* Coroutine과 Thread
구글에서는 AsyncTask를 코루틴으로 대체하여 쓰라고 하였기 때문에 잘못 생각하면 코루틴이 메모리누수가 없는 스레드라고 생각할 수 있다. 하지만 코루틴은 스레드가 아니다.
1. 메모리 구조의 차이
프로세스속에서 실행되는 독립된 여러 흐름중의 하나를 스레드라고 한다. 프로세스는 자기가 사용할 메모리영역(Heap)을, 이때 스레드는 Heap안에서 다시 자신만이 사용할 수 있는 고유의 메모리영역(stack)을 할당받게 된다.
그런데 코루틴은 스택을 할당받지 않고 프로세스의 Heap 메모리를 공유하여 사용하기 때문에 비동기 작업을 구현한다는 점에서 스레드와 동일한 일을 하고 있지만, 스레드보다는 함수에 가까운 구조를 가지고 있다.
2. 수행방식의 차이
코루틴은 비선점형 멀티테스킹이고 스레드는 선점형 멀티태스킹이다. 스레드는 실제로 멀티코어를 사용함으로써 동시에 복수의 스레드를 처리할 수 있는데 이것을 병행성 이 있다고 한다. 그런데 코루틴은 CPU 시간을 적절히 분할하여 사용하기 때문에 실제로는 복수의 작업을 동시에는 처리하지 않는다. 따라서 코루틴은 병행성이 없다. 하지만 코루틴은 한번에 한개밖에 실행이 되지 않음에도 불구하고 ContextSwitch가 없어 전환속도가 빠르기 때문에 외부에서 볼 때는 마치 동시에 처리되는 것처럼 인식되므로 동시성이 있다고 한다.
3. 코루틴의 장점
예를들어 세개의 스레드를 사용해야 하는 작업을 코루틴 세개로 만들어 사용할 경우 스택을 따로 할당할 필요가 없으니 사용되는 메모리가 줄어들게 된다. 그리고 스레드끼리 처리순서를 조정할 때 메모리를 공유하지 않기 때문에 수행해야 했던 context switching을 하지 않게 되므로 작업전환시의 오버헤드도 줄어들게 된다. 한 개의 스레드 안에서 여러개의 코루틴이 돌아가도록 할 수 있으니 스레드를 불필요하게 많이 만들어야할 필요도 없어지게 된다.
4. 코틀린에서의 사용
코틀린의 코루틴 안에서 실행되는 함수에는 suspend 키워드를 붙여서 실행을 보류하거나 재개할 수 있다는 표시를 하게된다. suspend키워드로 마킹된 함수는 Continuation Passing Style로 변환이 되고, Coroutine Builder를 통해 적절한 스레드 상에서 시나리오에 따라 동작하도록 구성된다.
* Coroutine 구조
코틀린의 코루틴은 크게 Coroutine Scope , Coroutine Context , Coroutine Builder 의 세 부분으로 나눌 수 있다.
1. Coroutine Scope
코루틴의 동작하는 범위를 규정한다. 스코프 내에서 실행되는 코루틴의 실행을 감시하거나 취소할 수 있다.
- Coroutine Scope
다음과 같은 인터페이스로 정의되며 특정한 dispatcher를 지정하여 동작이 실행될 스코프를 제한할 수 있다.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
- Global Scope
GlobalScop는 CoroutineScope의 한 종류이다. 안드로이드에서 어플리케이션 라이프리사이클을 따르며, 싱글톤으로 최사위 레벨에서 코루틴을 시작하기 때문에 필요할때 만들어 쓰고 버린다는 사용법이 불가능하다.
일반적으로는 사용자체를 추천하지 않는다.
2. Coroutine Context
코루틴은 항상 Coroutine Context로 구성된 콘텍스트 안에서 실행되는데, 이 콘테스트는 Dispatchers와 Job으로 구성된다.
* Dispatchers.Default
- CPU자원을 많이 필요로하는 작업에 사용된다.
- 공유 백그라운드 스레드의 common pool에서 동작한다.
- 동시작업 가능한 최대 개수는 CPU코어수와 같으며 최소 2개이다.
* Dispachers.IO
- 파일 혹은 소켓IO등의 가볍고 빈번한 IO작업에 사용한다.
- Blocking IO용 공유 스레드풀에서 동작한다.
- 필요에 따라 스레드를 추가 생성하거나 없앨 수 있는데 64 OR 코어 수 중 큰 수 만큼 생성이 가능하다.
- Dispatchers.Default와 스레드를 공유하므로 withContext에서 Dispatcher 변경시 context switching하지 않고 동일한 스레드에서 실행이 된다.
* Dispatchers.Main
- MainCoroutineDispatcher 클래스의 인스턴스이다.
- 안드로이드에서는 UI 오브젝트를 다루는 메인스레드에서 동작한다.
- 일반적으로 싱글스레드가 된다.
* Dispatchers.Unconfined
- 첫번째 지연점까지만 실행된다.
- 메인스레드에서 동작한다.
- 일반적인 용도로는 사용하지 않는다.
- Job & Deferred
코틀린에서는 코루틴 작업을 Job 혹은 Deferred라는 오브젝트로 만들어 다룬다. Deferred는 결과값을 가지는 Job이므로 실제로는 둘 다 Job이라고 볼 수 있다. 코루틴이라는 것은 프로그램의 흐름이라고 하는추상적인 개념인데, 코루틴 한 덩어리를 한 개의 Job이라는 오브젝트로 만들게 되면 그 오브젝트에 대해 취소나 예외처리를 함으로써 용이하게 코루틴의 흐름제어를 할 수 있게 된다.
val job = scope.launch {
// New coroutine
}
코투린은 일시정지 될 수 있는 작업의 흐름이기 때문에 Job은 코루틴의 여러가지 상태를 반영할 수 있도록 다음과 같이 설계되어있다.
그리고 각 상태는 다음과 같이 전환이 된다.
Job states cycle
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
Job 객체에 대해서는 cancel, join, start 등의 메소드가 정의되어 있다. cancel 과 start 는 위의 다이어그램에서 표시되는 cancel과 start 동작을 구현하는 메소드이다. join은 코루틴을 병렬처리하지 않고 현재 job에 정의된 작업을 수행하는 동안 기다리도록 하는 메소드이다.
3. Coroutine Builder
코루틴을 시작하는 방법에는 4가지가 이다.
- launch: 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과를 반환할 필요가 없는 작업에 사용하며 Job 객체를 반환한다.
- async: 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과를 반환할 필요가 있는 작업에 사용하며 Deferred 객체를 반환한다.
- runBlocking: 메인 스레드를 블록하고 작업을 실행한다. runBlocking은 테스트 용도 등 에나 사용하지, 코루틴을 위해서는 사용하지 말라고 권장하고 있다.
- withContext: 예를들어 Dispatchers.Main으로 지정된 스코프 안에서 Dispatchers.IO가 필요한 처리를 해야할 일이 있을 수 있다. 이때 Dispatchers안에 다시 Dispatchers를 정의할 수도 있지만 withContext를 사용하면 Dispatchers를 간편하게 스위치 할 수 있다. withContext를 이용한 스코프전환은 OS에서 관리되므로 오버헤드가 적다고 알려져 있다.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
* Coroutine 지연예외처리
- delay: milisecond단위로 루틴을 잠시 대기시킵니다. Thread.sleep은 스레드 자체를 정지시키는데 반해, delay는 코루틴이 멈추지 않고 대기상태에 들어간다는 점이 다르다.
- join: Job의 실행이 끝날때까지 대기시킨다.
- await: Deferred의 실행이 끝날때까지 대기시키고 결과값을 반환한다.
* Coroutine 취소
코루틴 실행을 취소하는데에는 몇가지 방법이 있다.
- cancle: 위에서 설명한대로 job을 Cancelling (transient state)로 변화시킨다.
- cancleAndJoin: job을 캔슬하고 Cancelled (final state)가 될 때까지 기다린다.
- withTimeout: 제한시간을 설정하고 그때까지 처리가 끝나지 않았을 경우 블럭을 취소하고 TimeoutCancellationException을 throw한다.
- withTimeoutOrNull: withTimeout을 처리중 제한시간이 경과되었을 경우 예외 대신 null을 반환한다.
* Coroutine 예외처리
- CoroutineExceptionHandler를 이용하여 코루틴 내부의 기본 catch block으로 사용할 수 있다.
- launch, actor: exception발생 시 바로 예외가 발생한다.
- async, produce: 중간에 exception이 발생하면 부모의 코루틴까지 모두 취소시킨다. 이는 structured concurrency를 유지하기 위함으로 CoroutineExceptionHandler를 설정해도 막을 수 없다.
- 자식 코루틴에서 exception이 발생하면 다른 자식 코루틴 및 부모코루틴이 다 취소되버리기 때문에, 문제가 생긴 코루틴만 exception 처리할 수 있도록 하기 위해 CoroutineExceptionHandler를 설정한다. 단, CancellationException는 handler에서 무시된다.
- 여러개의 exception이 발생하면 가장 먼저 발생한 exception이 handler로 전달되며 나머지는 무시된다.
'안드로이드(Kotlin)' 카테고리의 다른 글
Kotlin 코틀린의 Sealed Class (0) | 2023.03.17 |
---|---|
Kotlin 코틀린의 Flow (0) | 2023.03.17 |
Kotlin 코틀린의 object VS companion object (0) | 2022.08.30 |
Kotlin 코틀린의 싱글톤(Singleton) (0) | 2022.08.30 |
Kotlin 코틀린의 object와 class 키워드 (0) | 2022.08.30 |