Coroutines

Jini.Dev·2022년 6월 29일
0

코루틴 특징

코루틴의 특징은 흐름을 중간에 지연시켰다가(suspend) 다시 재시작하는(resume) 것이 가능합니다.
따라서 코루틴을 사용하면 루틴이 실행 되었더라도 더 바쁜 다른 루틴이 실행되는동안 잠시 멈췄다가, 바쁜게 끝나면 재시작하여 나머지 작업을 끝내는 형태의 비동기 프로그래밍이 가능하게 됩니다.

Suspend function

코루틴에서 실행할 수 있는 메소드를 만들때 suspend 키워드를 사용한다.

suspend function은 스레드와 스케쥴의 관리를 수행하는 것이 아니라 비동기 실행을 위한 일시정지(suspension) 지점을 정의하는 것이다.
기본적으로 메인스레드에서 동작하며, 코루틴 내에서만 호출할 수 있고 코루틴 스코프 안에서 실행된다.

코루틴과 스레드

구글에서 AsyncTask를 코루틴으로 대체하여 쓰라 권장 하기 때문에 코루틴은 AsyncTask의 약점인 메모리누수가 없는 스레드라고 생각할수있지만, 코루틴은 스레드가 아니다.

Coroutine 은 Thread 의 대안이 아니라, Thread 를 더 잘게 쪼개어 사용하기 위한 개념이다.
(작업의 단위를 Object로 축소하면서 하나의 Thread가 다수의 코루틴을 다룰 수 있다.)

코루틴의 장점

  1. 경량 : 기존의 스레드는 Context Switching을 통해 작업의 전환을 하지만, 코루틴은 스레드가 아닌 서브루틴을 일시중단(suspend)하는 방식이기 때문에 Context-switching에 비용이 들지 않는다.
    실행중인 스레드를 차단하지 않는 suspend를 지원하므로 단일스레드에서 많은 코루틴을 실행할 수 있다.
  2. 메모리 누수 감소 : 구조화된 동시실행을 사용하여 범위 내에서 작업을 실행한다.
  3. 처리 도중 취소 가능 : 실행중 cancle()을 호출하여 실행을 멈출수 있다. 실행중인 코루틴의 계층구조를 통해 자동으로 취소가 전달된다. (상위 코루틴 실행이 중단되면 모든 자식 코루틴이 중단된다.)
  4. Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있다. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공한다. (ViewModelScope, LifecycleScope등)

*) 구조화된 동시성 (Structured concurrency)

  • scope로 생성된 job에서 해당 scope를 사용하여 생성된 모든 코루틴은 job의 자식이 되며, 자식코루틴 중 하나가 예외를 던지면 부모job이 취소되어 코루틴 실행이 중단되고 결국 모든 자식 코루틴이 취소되는 것.
  • cancel 또는 예외로 인한 실패로 부모가 취소되면 모든 자식이 즉시 취소된다.
  • 부모는 자식이 완료 또는 취소상태로 완료될때까지 기다린다.

코틀린-코루틴 구조

코틀린의 코루틴은 크게 Coroutine Scope, Coroutine Context, Coroutine Builder의 세 부분으로 나눌 수 있다.
새로운 코루틴을 생성, 시작 실행하기 위해서 Coroutine Scope와 Coroutine Builder가 필요하며 생성시 코루틴에 대한 정보는 Coroutine Context에 담는다.

1. Coroutine Scope

코루틴이 동작하는 범위를 규정한다. 스코프 내에서 실행되는 코루틴의 실행을 감시하며 실행을 취소하거나, 실패 시 예외를 처리할 수 있다.

1.1 CoroutineScope

특정한 dispatcher를 지정하여 동작이 실행될 스코프를 제한할 수 있다.

1.2 GlobalScope

GlobalScope는 CoroutineScope의 한 종류로 프로그램 어디서나 제어 동작이 가능한 범위이다.
안드로이드의 경우 어플리케이션 라이프사이클을 따르며, 싱글톤으로 최상위 레벨에서 코루틴을 시작하기 때문에 필요할때 만들어 쓰고 버린다는 사용법이 불가능하다.

+) Jetpack 라이브러리에서 지원하는 Lifecycle-aware Custom Scope

  1. ViewModelScope : viewmodel이 삭제되면 자동으로 취소된다.
  2. LifecycleScope : Lifecycle객체에서 정의되며 Lifecycle이 끝날때 취소된다.
  3. Livedata : Livedata Builder에서 suspend 함수를 사용할 수 있으며, 비활성화가 되면 구성가능한 제한시간 후 자동으로 취소된다.

2. Coroutine Context

코루틴은 항상 Coroutine Context(코루틴을 어떻게 처리할 것인지에 대한 여러가지 정보의 집합)로 구성된 콘텍스트 안에서 실행되는데, 이 콘텍스트는 Dispatchers와 Job으로 구성되어 있다.

2.1 Dispatchers

Dispatchers는 코루틴이 실행될 스레드를 지정하는 역할을 한다.

  • Dispatchers.Default
    • CPU자원을 많이 필요로하는 Read/Wrtie, json파싱 등의 작업에 사용된다.
    • 공유 백그라운드 스레드의 common pool에서 동작한다.
    • 동시작업 가능한 최대 개수는 CPU코어수와 같으며 최소 2개이다.
    • • (Android) Application의 생명주기에 따른다.
  • Dispatchers.IO
    • Network, 파일IO, 소켓IO등 가볍고 빈번한 IO작업에 사용된다.
    • Blocking IO용 공유 스레드풀에서 동작한다.
    • 필요에 따라 스레드를 추가 생성하거나 없앨 수 있는데, 64 or 코어 수 중 큰 수만큼 생성 가능하다.
    • Dispatchers.Default와 스레드를 공유하므로 withContext에서 Dispatcher변경시 context switching하지 않고 동일한 스레드에서 실행된다.
  • Dispatchers.Main
    • suspend 함수 호출, LiveData 업데이트, UI작업 등에 사용된다.
    • 안드로이드에서는 UI 오브젝트를 다루는 메인스레드에서 동작한다.
    • 일반적으로 싱글스레드가 된다.
  • Dispatchers.Unconfined
    • 첫번째 지연점까지만 실행된다.
    • 메인스레드에서 동작한다.
    • 일반적인 용도로는 사용하지 않는다.
    • 호출한 컨텍스트를 기본으로 사용하지만 중단 후 다시 실행될때 컨텍스트가 변경되면 변경된 컨텍스트를 따라간다.

2.2 Job & Deferred

코틀린에서는 코루틴 작업을 Job 혹은 Deferred라는 오브젝트로 만들어 다룬다. (Deferred는 결과값을 가지는 Job이므로 실제로는 둘 다 Job이라고 볼 수 있다)

코루틴 한 덩어리를 한 개의 Job이라는 오브젝트로 만들게 되면 그 오브젝트에 대해 취소나 예외처리를 함으로써 코루틴의 흐름제어를 할 수 있게 된다.

Job은 코루틴의 여러가지 상태를 반영할 수 있는 cancel(), join(), start() 등의 메소드가 정의되어 있다.
join()은 코루틴을 병렬처리하지 않고 모든 자식job이 완료될 때 까지 job과 연관된 코루틴을 정지한다.

3. Coroutine Builder

코루틴블럭을 생성

  • launch(non-blocking) : 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과를 반환할 필요가 없는 작업에 사용하며 Job 객체를 반환한다.
    그 자리에서 바로 예외를 발생시키며, join()을 사용하여 job의 작업이 완료될때 까지 기다릴 수 있다.
  • async(non-blocking) : 메인 스레드를 블록하지 않는 코루틴 작업을 실행한다. 결과 반환이 필요한 작업에 사용하며 Deferred 객체를 반환합니다.
    await()를 만나면 실행 or 예외를 발생시키며, await()는 deffered객체가 완료 된 후 그 값으로 다음을 진행한다.
  • runBlocking(blocking) : 메인 스레드를 블록하고 작업을 실행한다.
    코루틴을 생성한 후 코루틴이 끝나고 그 결과값을 반환할 때까지 현재 스레드를 블락하기떄문에 테스트 용도등에나 사용하지, 코루틴을 위해서는 사용하지 말라고 권장하고 있습니다.
  • withContext : CoroutineContext를 변경할 때(컨텍스트 스위칭) 사용한다.
    항상 suspend fun안에서 사용되며, async와 동일한 역할을 하지만 await를 호출할 필요가 없다.
    컨텍스트 스위칭 ex) Dispatchers.Main으로 지정된 스코프 안에서 Dispatchers.IO가 필요한 처리를 해야할 일이 있을 수 있습니다. 이 때 Dispatchers 안에 다시 Dispatchers를 정의할 수도 있지만 withContext를 사용하면 Dispatchers를 간편하게 스위치할 수 있다.

코루틴 지연

  • delay : milisecond단위로 루틴을 잠시 대기시킨다. 
    Thread.sleep은 스레드 자체를 정지시키지만, delay는 코루틴이 멈추지 않고 대기상태에 들어간다는 점이 다르다.
  • join : Job의 실행이 끝날때까지 대기한다.
  • await : Deferred의 실행이 끝날때까지 대기시키고 결과값을 반환한다.

코루틴 취소

  • cancel() : job을 Cancelling상태로 변화시킨다.
  • cancelAndJoin() : cancel() 과 join()을 한 번에 할 수 있는 메소드. job을 캔슬하고 Cancelled상태가 될 때까지 기다린다.
  • withTimeout : 제한시간을 설정하고 정해진 시간 안에 처리가 끝나지 않았을 경우 블럭을 취소하고 TimeoutCancellationException을 throw한다.
  • withTimeoutOrNull : withTimeout을 처리중 제한시간이 경과되었을 경우 예외 대신 null을 반환한다.

예외 처리

  • CoroutineExceptionHandler를 이용하여 코루틴 내부의 기본 catch block으로 사용할 수 있다.
  • launch, actor : exception발생 시 바로 예외가 발생.
  • async, produce : 중간에 exception이 발생해도 await를 만나야 비로소 exception이 발생.
  • Job.cancel()을 제외한 다른 exception이 발생하면 부모의 코루틴까지 모두 취소시킨다. 이는 구조화된 동시성을 유지하기 위함으로 CoroutineExceptionHandler를 설정해도 막을 수 없다.
  • 자식 코루틴에서 exception이 발생하면 다른 자식 코루틴 및 부모코루틴이 다 취소되버리기 때문에, 문제가 생긴 코루틴만 exception 처리할 수 있도록 하기 위해 CoroutineExceptionHandler를 설정한다.
  • 여러개의 exception이 발생하면 가장 먼저 발생한 exception이 handler로 전달되며 나머지는 무시된다.

Coroutine Flow를 이용한 리액티브 프로그래밍

Coroutine의 Flow는 데이터 스트림이며, 코루틴상에서 리액티브 프로그래밍을 지원 하기 위한 것이다.

Flow 타입의 생성은 flow{ } 빌더를 사용하며, Flow{ ... } 블록 내부 코드는 중단(suspend) 가능하다.
Flow{ } 빌더의 코드블럭은 플로우가 수집(Collect)되기 전까지 실행되지 않는다 (콜드 타입)
Producer(생산자), Intermediary(중간 연산자), Consumer(소비자) 로 구성된다.

1. 생산자 - Flow Builder

데이터를 발행하는 역할을 한다. flow{ } 블록 내부에서의 emit()을 통해 데이터를 생성한다.

  • flow { } : 가장 기본적인 플로우 빌더
  • flowOf { } : 고정된 값들을 방출하는 플로우를 정의
  • .asFlow() : 다른 Collection/Sequence 들을 Flow로 변환
    ex) List / Map / Array 를 flow로 변환
fun getData() : Flow<T> = flow {  //flow 블록 선언
    val data = getDataApi.getData() //데이터 받아오기
       emit(data)     //데이터 발행(생성)
}

2.1 중간 연산자 - Intermediate flow operators

생성된 데이터를 수정. 업스트림 플로우에 적용되어 다운스트림 플로우를 반환한다.
suspend(중단)함수가 아니기에 새롭게 변환된 플로우를 즉시 반환한다. (중간 연산자(map, filter등) 블록 내부에서 suspend(중단)함수 호출 가능)
중간 연산자 콜드타입으로 동작한다.

  • map : 지정된 transform 함수를 원래 flow의 각 값에 적용한 결과가 포함된 flow를 반환
  • filter : 데이터 필터링, 지정된 값과 일치하는 값만 포함하고 있는 flow를 반환한다.
  • onEach : 모든 데이터마다 연산 수행
  • combine : 각 flow에서 가장 최근에 방출한 값을 조합하여 인자로 주어진 transform 함수와 함께 값이 생성된 flow를 반환한다.
  • debounce : 원래 flow와 동일한 flow를 반환하지만, 지정된 제한 시간안에 새로운 값 뒤에 오는 값은 필터링합니다. 항상 최신 값이 방출된다.
  • drop : 인자로 주어진 count가 음수일 경우 예외를 던지며, 값을 방출하기 시작할 때 주어진 count 값 만큼 방출값을 무시하는 flow를 반환한다.

등의 중간 연산자가 있다.

Flow

suspend fun performRequest(request: Int): String {
delay(1000) 
return "response $request" // 플로우 값 매핑 "request -> response $request"
}

fun main() = runBlocking<Unit> {
(1..3).asFlow() // .asFlow() - 배열을 Flow로 변환
.map { request -> performRequest(request) } // 업스트림 플로우를 매핑해서 다운스트림 플로우 반환
.collect { response -> println(response) } // 중간 연산자(map)로 반환된 새 플로우를 수집(collect)
}

2.2 변환 연산자 - Transform operator

중간 연산자(map, filter) 같이 단순한 변환 보다 복잡한 변환을 처리위한 연산자.

중간 연산자는 요소마다 1개의 변환밖에 하지 못하지만, 변환 연산자(transform)은 emit()을 추가하여 요소마다 여러개의 변환이 가능하게 해준다.

fun main() = runBlocking<Unit> {
	(1..3).asFlow()
	    	.transform { request ->		// 변환 연산자(transform)
	        	emit("Making request $request") 	// emit() - flow 방출
	        	emit(performRequest(request)) 		// emit() - flow 방출
	    	}.collect { response -> println(response) }	// flow 수집
}

3. 소비자 - Terminal flow operator

플로우 수집을 시작하는 종단 함수

  • collect
  • toList 또는 toSet : Flow를 Mutable Collection으로 변환
  • first : 첫 번째 원소를 반환하고 나머지는 Cancel, 첫 번째 요소만 처리할 때
  • collectIndexed : collect와 같은 동작이지만 index 요소가 추가되어 원하는 index에 맞는 처리가 가능
  • collectLatest : 새로운 데이터가 들어오면 이전 데이터의 처리를 강제 종료시키고 새로운 데이터를 처리

등의 연산자가 있다.

profile
정신 차려보니 개발자가 되어있었다.

0개의 댓글