본문 바로가기

개발

[kotlin] Kotlin Coroutine - 1

코루틴이 무엇인가요! 

- 코루틴은 동시성 프로그래밍을 가능케 하도록 만든 개념

 

이게 어떤게 좋은가요?

- 가독성 측면: Coroutine은 콜백 기반 코드를 sequential code로 바꾸어주기 때문에 비동기 코드를 단순화

- 쓰레드 nonblocking : 네트워크, DB작업등에 소요될때 쓰레드를 물고 있지 않는다. suspend 중단함수를 만나게 되면 현재 쓰레드를 물지 않고 해제한다. 다른 쓰레드에서 중단 함수의 수행이 완료되고 다시 쓰레드를 해제한다. 이후 또 다른 쓰레드에서 중단되었던 함수 다음에 재개한다.

 

📌 Thread vs Coroutine

- 애플리케이션에는 하나 이상의 프로세스가 있고, 각각의 프로세스는 적어도 하나의 스레드가 존재한다. 

- Coroutine은 스레드 안에서 실행된다. 같은 스레드에 10개, 100개의 Coroutine이 있을 수 있다.

- 동시성 프로그래밍에서 한 시점에는 하나의 Coroutine만 실행

- Coroutine은 특정 스레드에 종속되지 않는다. Coroutine은 resume될 때마다 다른 스레드에서 실행

- 스레드를 blocking 하지 않으면서 작업의 실행을 잠시 중단. 스레드 B가 스레드 A의 작업이 끝날때까지 대기해야하는 작업을 잠시 중단하고, 그동안 다른 작업을 할수 있다

- 스레드를 blocking하지 않아 더 빠른 연산으로 이어지게하고, 메모리 사용량을 줄여 많은 동시성 작업을 수행

 

CPS  - Continuation 

- Coroutine에는 CPS(Continuation Passing Style) 패러다임이 적용.

- CPS란 호출되는 함수에 Continuation을 전달하고, 각 함수의 작업이 완료되는 대로 전달받은 Continuation을 호출하는 패러다임을 의미합니다. 이러한 의미에서 Continuation을 일종의 콜백

- Continuation은 다음에 무슨 일을 해야 할지 담고 있는 확장된 콜백

- CPS에선 Continuation을 전달하면서, 현재 suspend된 부분에서의 resume을 가능하게 해준다. 

- Continuation은 resume되었을 때의 동작 관리를 위한 객체로, 연속적인 상태간의 communicator

- Continuation은 호출 함수간의 suspend-resume을 위한 communicator이고, CPS는 함수 호출 시에 이 Continuation을 전달하는 패러다임

 

State machine 

- Kotlin은 모든 중단 가능 지점에 라벨링 하고 재개시 label의 값을 when 으로 분기한다. 

- 중단 가능 지점에서 suspend되고 해당 작업이 끝나 resume되면 다시 다음 블록을 실행

- 상태를 관리하는 하나의 방법으로 상태머신


CoroutineScope (interface)

- 모든 코루틴은 Scope 내에서 실행이 되어야 한다. 즉 코루틴은 Coroutine Scope 을 통해서 코루틴이 사용되는 범위를 지정해줘야 한다. 

- 하나의 CoroutineContext 멤버속성만 갖는 인터페이스다.

- 코루틴 빌더들은 CoroutineScope의 확장함수로, 다양한 요구사항에 맞게 개별적인 Coroutine(코루틴)을 만든다.

- 전역 스코프 생성은 GlobalScope 와 CoroutineScope()이 존재한다.

 

GlobalScope

- GlobalScope는 CoroutineScope의 한 종류로써 가장 큰 특징은 Application이 시작하고 종료될 때까지 계속 유지가 된다. 

-  최상위 코루틴이며 GlobalScope.launch{} 실행한 코루틴은 어플리케이션이 종료될 때까지 살아있다. 

-  전역 스코프라서 사용하는 즉시 새로운 스레드가 생성되고 그 안에서 비동기 작업이 가능해진다.

- EmptyCoroutineContext를 기본 컨텐스트로 갖고 있다. 

- Singletone이기 때문에 따로 생성하지 않아도 되며 어디에서든 바로 접근이 가능하여 간단하게 사용하기 쉽다는 장점이 있다.

- GlobalScope를 사용하면 메모리 누수의 원인이 될 수 있기 때문에 신중히 사용해야 한다. 즉 다시 말해 앱이 실행된 이 후 계속 수행이 되어야 한다면 GlobalScope 를 사용해야 하는 것

 

CoroutineScope (function)

- CoroutineScope()는 코루틴 블럭(영역)을 생성하는 역할

- CoroutineScope()는 서버와 통신한다거나 하는 등의 필요할 때만 시작, 완료되면 종료하는 용도로 사용된다.

- CoroutineScope()는 객체를 생성하기 때문에 호출한 객체가 메모리에서 해제가 되면 같이 파괴가 됩니다. 하지만, 원하는 대로 작동하려면 적절하게 cancel을 호출해줘야 해당 코루틴 작업이 취소된다. 

 

GlobalScope vs CoroutineScope()

- GlobalScope는 EmptyCoroutineContext를 CoroutineContext로 사용

- CoroutineScope()는 CoroutineContext가 강제되고, 넘겨 받은 CoroutineContext에 Job이 없으면 추가 해준다.

 

📌 CoroutineContext의 유무 때문에 생기는 차이점

- Dispatcher : EmptyCoroutineContext로 코루틴 람다를 실행하면 기본적으로 Dispatchers.Default를 지정

GlobalScope.launch {} 를 하면 Dispatcher.Default로 지정되어 코루틴 람다를 실행, CoroutineScope는 생성할 때 넘겨준 Dispatcher로 실행

- Job : GlobalScope는 Job이 없는 CoroutineContext를 가지고 있기 때문에, GlobalScope.cancel() 이런 식으로 코루틴 작업을 취소할 수 없다. GlobalScope.launch 에서 반환되는 job을 가지고만 취소 해야만 한다.

CoroutineScope()는 생성할 때 Job이 있는지 체크하여 없으면 Job을 넣어주기 때문에, 바로 cancel이 가능하다.

 

CoroutineContext

- CoroutineContext는 Job, CoroutineName, CoroutineDispatcher, CoroutineInterceptor, CoroutineExceptionHandler 등과 같은 Element 인스턴스를 순서가 있는 Set(indexed set)으로 관리

- Dispatcher 는 코루틴에 대한 task수행 분배를 어떻게 할 것인지 결정하는 역할, 정확히는 Interceptor 에 의해서 Coroutine 은 동작에 대한 중지 혹은 동작에 대한 재개를 실행

- CoroutineContext는 Coroutine이 실행되는 환경

public interface CoroutineContext {
  /**
   * Returns the element with the given [key] from this context or `null`.
   * Keys are compared _by reference_, that is to get an element from the context the reference to its actual key
   * object must be presented to this function.
   */
  public operator fun <E : Element> get(key: Key<E>): E?
  /**
   * Accumulates entries of this context starting with [initial] value and applying [operation]
   * from left to right to current accumulator value and each element of this context.
   */
  public fun <R> fold(initial: R, operation: (R, Element) -> R): R
  /**
   * Returns a context containing elements from this context and elements from  other [context].
   * The elements from this context with the same key as in the other one are dropped.
   */
  public operator fun plus(context: CoroutineContext): CoroutineContext = ...impl...
  /**
   * Returns a context containing elements from this context, but without an element with
   * the specified [key]. Keys are compared _by reference_, that is to remove an element from the context
   * the reference to its actual key object must be presented to this function.
   */
  public fun minusKey(key: Key<*>): CoroutineContext
}

CoroutineContext 구현체

- CoroutineContext는 인터페이스로 이를 구현한 구현체는 기본적으로 3가지의 종류가 존재

 

1) EmptyCoroutineContext - Default Context / Singleton

- EmptyCoroutineContext 는 구현해야할 모든 CoroutineContext 멤버 함수들에 대해서 기본 구현만 정의한 컨텍스트

- 이 기본 컨텍스트는 어떤 생명주기에 바인딩 된 Job 이 정의되어 있지 않기 때문에 애플리케이션 프로세스와 동일한 생명주기를 갖게 된다. 


2) CombinedContext - 두개 이상의 컨텍스트가 명시되면 Context간 연결을 위한 컨테이너 역할의 Context.

- 각각의 요소를 + 연산자를 이용해 연결하고 있는데 이는 앞서 설명한 것처럼 CoroutineContext가 plus 연산자를 구현하고 있기 때문이다. ex: launch(Dispatchers.Main + job) 

- Element + Element + … 는 결국 하나로 병합 된 CoroutineContext (e.g. CombinedContext)를 만든다. 

 

CombinedContext

3) Element - Context의 각 요소(Element)들도 CoroutineConext를 구현

 

Key / Element

- Key는 Element 타입을 제네릭 타입

- Element CoroutineContext를 상속하며 Key를 멤버 속성

- 코루틴 컨텍스트를 구성하는 Element는 CoroutineContext는 Job, CoroutineName, CoroutineDispatcher, CoroutineInterceptor, CoroutineExceptionHandler

- Element들은 각각의 Key를 기반으로 CoroutineContext에 등록

- CoroutineContext는 Element들이 등록될 수 있고, 각 요소들이 등록될 때 요소의 고유한 Key를 기반으로 등록된다.

/**
 * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
 * Keys in the context are compared _by reference_.
 */
public interface Key<E : Element>

/**
 * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
 */
public interface Element : CoroutineContext {
  	/**
   	* A key of this coroutine context element.
   	*/
  	public val key: Key<*>
  
  	...overrides...
}

 

📌 CoroutineContext plus

Dispatcher: 코루틴이 실행될 쓰레드 풀을 잡고 있는 관리자

CoroutineExceptionHandler: 코루틴에서 Exception이 생겼을 때의 처리

CoroutineContext합치기
CoroutineContext Key로 조회(실제 String값 X)

- Key("KeyB")를  요청하면 CoroutineExceptionHandler를 반환받을 수 있다.

 

Dispatcher

- CoroutinScope의 경우 GlobalScope와는 다르게 Dispatcher 라는 것을 지정할 수 있는데, 이는 코루틴이 실행될 스레드를 지정하는 역할을 한다. 

- 종류 : 

Dispatchers.Default: CPU를 많이 쓰는 작업에 최적화. (데이터 정렬, 복잡한 연산 등..)
Dispatchers.IO: 이름처럼 IO 작업을 할 때에 최적화. (다운로드, 파일 입출력, 네트워킹, DB 작업 등..)
Dispatchers.Main: 메인 스레드. (화면 UI 작업)
Dispatchers.Unconfined: 호출한 context를 기본으로 사용하는데 중단 후 다시 실행될 때 context가 바뀌면 바뀐 context를 따라가는 특이한 Dispatcher.


CoroutineBuilder

- 코루틴 빌더들은 CoroutineScope의 확장함수로, 다양한 요구사항에 맞게 개별적인 Coroutine(코루틴)을 만든다.
- Coroutine Builder에는 여러 종류 존재 ( launch / async / withContext / runBlocking / actor / produce )

 

빌더는 다음 이시간에 :0 ) 

'개발' 카테고리의 다른 글

[kotlin] Kotlin Coroutine - 2  (0) 2023.01.18
[kotlin] Coroutines under the hood - 번역  (0) 2022.12.28
[kotlin] 코루틴 개념 정리  (0) 2021.11.09