[Tistory] [TIL] Kotlin Coroutine의 구조와 간단한 예제

원글 페이지 : 바로가기

-1- Coroutine Coroutine이란? Coroutine은 비동기적으로 실행되는 코드를 간소화하기 위한 실행 설계 패턴으로 mainroutine과 subroutine에 suspend와 resume을 통해 routine들 간의 비선점형 멀티태스킹을 할 수 있도록 하는 프로그램 구성요소이다. 선점형, 비선점형 멀티태스킹?? 선점형 멀티태스킹 : 멀티코어를 사용해서 동시에 여러가지 작업을 하는 것으로 하나의 프로세스가 다른 프로세스 대신에 프로세서를 강제로 차지할 수 있어 병행성이 있다 – 쓰레드 비선점형 멀티태스킹 : 하나의 프로세스가 CPU를 할당받으면 종료되기 까지 다른 프로세스가 CPU를 강제로 차지할 수 없어 동시성은 있지만, 복수의 작업을 동시에 처리하는 것이 아니라 병행성은 없다. – 코루틴 Thread와 Coroutine의 차이점 수행 방식의 차이 위에서 작성한대로 쓰레드는 선점형 방식이며 코루틴은 비선점형 멀티 태스킹 방식으로 처리한다. 메모리 구조의 차이 쓰레드 – Stack 메모리에 할당 코루틴 – Heap 메모리에 할당 비동기 작업을 함에 Thread와 같은 목적이지만 coroutine을 사용하는 이유 메모리 효율 차이 2000개 미만의 스레드에는 1.5GB 이상의 메모리가 필요하다. 100만 개의 코루틴은 700MB 미만의 메모리가 필요하다 이처럼 코루틴은 스레드를 사용하는 것보다 메모리 상으로도 매유 효율적이다. 가독성 향상 스레드의 형식으로 작업을 하게 되면 콜백 함수나 복잡한 콜백 체인등이 필요로 한데, coroutine은 선형 스타일로 작성할 수 있도록 도와줘 비동기 작업의 흐름을 이해하기가 쉽다. 메모리 누수 방지 비동기 작업을 시작, 중지, 재개, 취소 할 수 있어 비동기 코드의 실행을 적재 적소에 사용할 수 있게 된다 이로 인해 메모리 누수 같은 문제를 방지할 수 있다. Coroutine의 구조 [ 1 ] Scope Coroutine Scope Coroutine Scope는 코루틴의 수명 또는 지속시간을 정의하며 관리하는 인터페이스이다. 주로 사용되는 Scope로는 ViewModelScope, LifecyclerScope, GlobalScope가 있다. ViewModelScope ViewModel과 수명주기를 공유하여 ViewModel의 생명주기에 맞게 코루틴을 실행할 수 있도록 한다. 주로 데이터 베이스나 네트워크 요청과 같은 비동기 작업을 수행할 때 ViewModel과 함께 사용된다. LifecycleScope Activity나 Fragment의 LifecyclerOwner와 함께 사용되며, 해당 수명 주기 상태에 따라 자동으로 코루틴을 시작하거나 중지한다. Activity와 Fragment의 생명주기와 일치하는 범위 내에서 비동기 작업을 처리할 때 사용된다. GlobalScope GlobalScope는 앱 전체에 전역적으로 공유되는 Scope이다. 이때 GlobalScope는 앱의 수명 주기와 독립적으로 실행되며, 앱 전체에서 사용 가능하다 GlobalScope는 앱이 종료될 때까지 지속적으로 실행이 될 수 있으므로 메모리 누수등의 문제가 발생할 수 있으므로 예외처리 및 취소에 신경써야 한다. 때문에 GlobalScope는 일반적으로는 권장되지 않는다고 한다. Coroutine의 구조 [ 2 ] Context Coroutine Context는 Coroutine의 실행 환경을 정의하는 인터페이스이며 주요 요소는 Dispatcher, Job&Defferd이다. Dispatcher는 코루틴이 어떤 쓰레드 또는 쓰레드 풀에서 실행되는지를 결정한다. Dispatcher.Default CPU 바운드 작업( 계산 위주의 작업 ) 에 적합하다. 기본적으로 고정 크기의 스레드 풀을 사용하며 동시에 실행 가능한 스레드 수는 프로세서 코어 수와 관련. Dispatcher.IO 네트워크 요청, 파일 입출력 등 IO 바운드 작업에 적합하다 Job은 코루틴의 생명주기와 관련된 개념으로 작업의 상태를 추적하고 제어하는 데 사용된다. Job은 부모 – 자식 계층 구조로 구성될 수 있다. 자세한 내용 https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/ Coroutin Builder에서 launch 빌더를 통해 생성되며 Active, Completed, Cancelld 등의 상태로 전환될 수 있다. 이 외에도 디버깅에 유용한 CoroutineName, 포착되지 않은 예외를 처리하는 CoroutineExceptionHandler 기능 등이 있다. Coroutine의 구조 [ 3 ] Builder Coroutine Builder는 코루틴의 실행을 정의하는 함수이다 launch – 비동기 작업을 실행하는 가장 기본적인 빌더 반환값이 없다. 주로 네트워크 요청이나 파일 다운로드와 같이 IO 작업을 처리할 때 사용된다. async – 결과 값을 반환하는 코루틴 빌더이다. async를 사용하여 병렬로 여러 개의 작업을 실행하고, 각 작업의 결과를 모아서 처리할 수 있다. await() 함수를 통해 각 작업의 결과 값을 얻어올 수 있다. 이때 job이 아닌 deferred이 생성된다 async는 Exeption이 발생하더라도 await()까지 가서야 await()의 일부로 Exeption을 발생시킨다. 때문에 Logcat에 Exeptional 내용이 기록되지 않으니 주의해야한다. withContext – 현재 코루틴의 Context를 변경하여 다른 Dispatcher에서 코드 블록을 실행하는 데 사용되며 주로 스레드 전환과 관련된 작업에 사용되며 IO Dispatcher에서 네트워크 요청 후 MainDispatcher로 돌아와 UI를 업데이트 하는 등의 방식으로 사용할 수 있다. runBlocking – 코루틴이 완료될 때 까지 현재 스레드를 블록하는 빌더이다. 주로 테스트나 메인 함수에서 사용되며 비동기 코드를 동기적으로 실행하므로 테스트 할 때 유용하다. launch, async, withContext는 CoroutineScope의 확장 함수로 사용되며 runBlocking은 일반 함수로 사용된다. 추가로 디버깅에 유용한 CoroutineName과 포착되지 않은 예외를 처리하는 CoroutineExceptionHandler 기능도 있다. Coroutine Suspension Point CoroutineSuspensionPoint는 코루틴의 코드 진행 흐름에서 중단이 발생되는 지점을 말한다. SuspensionPoint는 코루틴의 핵심 목표인 동시성 확보를 위해 매우 중요하다. 특정 coroutineA가 네트워크 요청, 파일 입출력 등의 비동기 호출 시점인 SuspensionPoint에서 실행을 중단이 되면 다른 작업을 처리하는 동안 대기 상태로 전환되며 새롭게 생성되거나 대기중인 코루틴을 실행시킨다. SuspensionPoint를 사용하기 위해서는 suspend 키워드로 어노테이트된 함수여야하며 suspend 키워드로 어노테이트된 suspend fun 형식의 함수는 다른 suspend 함수, Coroutine Scope 내에서만 실행이 가능하다 Suspenstion Point는 delay()와 yield()를 통해서도 만들 수 있다. delay() – 현재 실행중인 작업을 밀리초만큼 멈추게 한다. yield() – 명시적인 지연을 만들지 않고 현재 작업이 더 중요한 작업들의 실행을 기다린다 위와 같이 함수를 이동하며 연속적인 함수에서의 비동기 작업은 Continuation Passing Style( CPS ) 프로그래밍 패러다임을 따라 명시적 제어를 전달함으로 코드의 흐름을 명확하게 만들 수 있다. suspensionPoint의 추가적인 내용은 매우 잘 정리되어져 있는 블로그가 있어서 추후에 학습 후 추가 예정 https://velog.io/@koo8624/Kotlin-Coroutine%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC [Kotlin] Coroutine의 동작 원리 본 글은 Kotlin Coroutine의 디자인을 제안한 문서를 참조하여 coroutine의 동작 원리에 대해 상세히 다루는 것을 목표로 하고 있습니다.원문에서는 coroutine을 한 문장으로 an instance of suspendable computation velog.io 라면을 끓이는 상황의 간단한 예제 내가 행하고 있는 행동을 메인 쓰레드라고 가정 했을 때 그림처럼 나타낼 수 있다. 라면 물이 끓지 않았는데 면을 넣고, 그릇에 면을 담을 수 없기 때문에 불을 켜고나서3 분 뒤와 면과 스프를 넣고 난 2분 뒤에 다음 함수가 실행되는 상황이다 이때 불을 켜고 3분간 기다리면서 핸드폰을 사용하다가 3분 뒤 면,스프 넣기를 실행해야 할 때 핸드폰 보기를 중단한다. 그리고 면과 스프를 넣고 난 뒤 라면이 조리되는 나머지 2분 동안 다시 휴대폰을 보고, 2분이 지나 그릇에 라면을 담아야할 때 휴대폰 보기를 중단한다. 각각 coroutineA ,B가 코드가 실행되는 순서를 보면 AABABA로 동시에 A,B 쓰레드를 작업하는 것처럼 보인다 이로인해서 coroutine은 동시성이 있다고 볼 수 있다. 예제의 상황을 간단하게 코드작성 fun main() {
// test용 runBlocking 생성
runBlocking {
// cook job 생성
val cook = launch {
cook()
}
// usePhone job 생성
val usePhone = launch {
usePhone()
}

cook.join()
usePhone.cancel()

end()
}
}

suspend fun cook(){
cooking()
inputRamen()
}
suspend fun usePhone() {

while (coroutineContext.isActive){ // 현재 Job의 상태를 확인하며 Active일 경우 지속.
println(“usePhone…”)
delay(1000)
}
}
suspend fun cooking(){
println(“turn on gas stove”)
delay(5000L)
}
suspend fun inputRamen(){
println(“InputRamen”)
delay(5000L)
}
fun end(){
println(“End”)
} runBlocking 블록 내에서 두개의 launch 빌더를 사요하여 cook 및 uesPhone 코루틴을 시작한다. cook.join() 으로 cook 코루틴이 완료될 때 까지 기다리고, cook 코루틴이 종료되면 usePhone.cancel()로 usePhone 코루틴을 취소 한다 suspend 함수인 cook, usePhone, cooking, inputRamen은 각각 일부 작업을 수행하고 delay 함수를 사용하여 지연시킨다. cook이 종료되면 end() 함수를 실행하며 종료 [오늘 복습한 내용] 1. 동기, 비동기 프로그래밍 [오류,에러 등등] 1. 코루틴 개념을 이해하려다가 머리에서 오류가 계속 났다 [느낀 점] 1. 이해가 안가는 문제는 다양한 시각의 의견을 들어보다보면 금방 해결된다. 2. 논리적으로 이해가 가지 않는 문제가 있으면 적거나 그려보면서 풀어보는 것이 좋다 3. 매일 매일 어렵긴 한데 재밌다 [Reference] // coroutine https://kt.academy/article/cc-builders https://sandn.tistory.com/110 https://junyoung-developer.tistory.com/104 https://dev.gmarket.com/82 https://wooooooak.github.io/kotlin/2019/08/25/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EA%B0%9C%EB%85%90-%EC%9D%B5%ED%9E%88%EA%B8%B0/ https://tech.wonderwall.kr/articles/CoroutineDeepDive/

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다