Coroutine 이란
동시성 프로그래밍 개념을 코틀린에 도입
Coroutine은 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드이다.
기존의 복잡한 AsyncTask 또는 다수 스레드 관리를 직접 해주지 않아도 되며,
기존 다중 스레드 보다 훨씬 더 효율적으로 동작한다.
특징으로는 스레드 위에서 실행되는 여러가지 코루틴이 존재한다고 할 때 Coroutine1,2,3 이 있다고 칠 때
1을 실행하던 중 2가 실행돼도 실행 중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로
다른 스레드로 전환하는 것이 아니라 기존 스레드를 유지하며 기존 스레드에서 2를 실행하게 된다.
이후 1을 다시 실행할 때 저장해둔 1 상태를 불러와 다시 스레드에서 1을 실행하게 된다.
한마디로 스레드의 멈춤없이 루틴을 돌릴 수 있게 되며 이는 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것보다 훨씬 적은 자원 소모를 하게 된다.
왜냐하면 스레드 관련 이벤트 또는 결과 처리를 위한 콜백 작성이 필요없고 순차적으로 코드를 작성하면 되기 때문이다. (사실 코루틴도 내부적으로는 여전히 다중 스레드를 사용한다.)
![](https://blog.kakaocdn.net/dn/EiXqf/btrrB5n8LX3/85dgaYOeL4OTJhJtbkVtG1/img.png)
안드로이드 스튜디오 최신 버전에는 코루틴이 기본적으로 내장되어 있지만 구버전을 사용하는 경우에 기본적으로 추가가 되어있지 않을 수 있다.
이때는 build.gradle 파일의 dependencies에 의존성을 추가해서 사용
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
Scope
모든 코루틴은 스코프 내에서 실행되어야 하는데 이를 통해서 Activity 또는 Fragment의 생명주기에 따라 소멸될 때 관련 Coroutine을 한 번에 취소할 수 있는데 이는 곧 메모리 누수를 방지한다.
Scope는 Custom 또는 이미 내장된 범위를 사용할 수 있다.
모든 코루틴은 항상 자신이 속한 스코프를 참조해야 한다. 이후에 cancel로 모두 취소 가능하다.
(Scope는 사실 CoroutineContext 타입 필드를 launch 등의 확장 함수 내부에서 사용하기 위한 매개체 역할만 담당)
Scope 종류
- GlobalScope : 앱의 생명주기와 함께 동작하기 때매 실행 도중에 별도 생명 주기 관리가 필요 없음. 시작~종료 까지 긴 기간 실행되는 코루틴의 경우에 적합
- CoroutineScope : 버튼을 눌러 다운로드 하거나 서버에서 이미지를 열 때 등. 필요할 때만 열고 완료되면 닫아주는 코루틴 스코프를 사용할 수 있다.
- ViewModelScope : Jetpack 아키텍처의 뷰모델 컴포넌트 사용 시 ViewModel 인스턴스에서 사용하기 위해 제공되는 스코프. 해당 스코프로 실행되는 코루틴은 뷰모델 인스턴스가 소멸될 때 자동으로 취소
CoroutineScope의 경우 Context를 지정할 수 있는데 이는 Coroutine이 실행될 스레드를 지정하는 것이다.
binding.startBtn.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
// DO IT
}
}
Context
실제로 Coroutine이 실행 중인 여러 작업(Job)과 Dispatchers를 저장하는 일종의 맵이라고 보면 된다.
이를 통해 코틀린 런타임은 다음에 실행할 작업을 고르고 어떤 스레드에 배정할지 결정
Dispatchers
코루틴 디스패쳐의 경우에는 Default, IO, Main, Unconfined 등 이 있다.
- Default : 안드로이드 기본 스레드풀 사용, CPU를 많이 쓰는 작업에 최적화 (데이터 정렬, 복잡한 연산 등)
- IO : 이미지 다운, 파일 입출력 등 입출력에 최적화되어있는 Dispatchers (네트워크, 디스크, DB 작업에 적합)
- Main : 안드로이드 기본 스레드 에서 코루틴 실행, UI 와 상호작용에 최적화
- Unconfined : 호출한 Context를 기본으로 사용하는데 중단 후 다시 실행될 때 컨텍스트가 바뀌면 바뀐 컨텍스트를 따라가는 Dispatchers
Dispatchers는 코루틴을 적당한 스레드에 할당하며, 코루틴 실행 도중 일시 정지 or 실행 재개를 담당
(다음에 어떤 코루틴을 실행시킬지 결정) 커스텀 스레드 풀을 위한 디스패처도 생성할 수도 있다.
Builder
Koltin은 Coroutine Builder에 원하는 동작을 람다로 넘겨 Coroutine을 생성하여 실행하는 방식을 사용
Coroutine에서 제공하는 빌더
launch
현재 스레드를 차단없이 Coroutine 즉시 실행, 특정 결과값을 반환하지 않고 Job 객체를 반환 (실행후 망각 코루틴)
CoroutineScope(Dispatchers.Default).launch {
delay(1000)
Log.d("coroutine", "launch success")
}
async
현재 스레드 중단없이 Coroutine을 즉시 시작, 호출 쪽에서 await()을 통해 결과를 기다릴 수 있다.
다수의 Coroutine을 사용할 때 사용 async 빌더는 suspend 함수 내부에서만 사용 가능하다.
ex) 연산 시간이 오래 걸리는 2개의 네트워크 작업의 경우를 예를 들면 2개의 작업이 모두 완료되고 나서 이를 처리
이때는 async 작업이 모두 완료되고 나서야 await() 호출 줄 코드가 실행된다.
CoroutineScope(Dispatchers.Default).async {
val deferred1 = async {
delay(500)
350
}
val deferred2 = async {
delay(1000)
200
}
Log.d("coroutine", "${deferred1.await() + deferred2.await()}")
}
runBlocking
coroutine을 시작시키고 완료될 때까지 현재 스레드를 중단
Coroutine의 취지와 정반대이지만 Test, Legacy Code 및 Library 통합 시 유용
Scope의 확장 함수가 아니여서 CoroutineScope 없이도 실행이 가능
runBlocking {
delay(3000)
Log.d("coroutine", "Thread ${Thread.currentThread()}")
}
supervisorScrope
coroutineScope와 비슷하지만 Coroutine이 실패해도 다른 Coroutine이 취소 되지 않는다.
suspend fun supervisor(){
CoroutineScope(Dispatchers.IO).launch{
suspervisorScope{
val firstJob = launch{ throw AssertionError("firstJob AssertionError cancel") }
val secondJob = launch(Dispatcher.Default){
delay(1000)
println("secondJob is Alive")
}
firstJob.join()
secondJob.join()
}
}.join()
}
produce, actor / Coroutine Channel 공부 필요, 예정
produce
정해진 채널로 Data를 Stream으로 보내는 Coroutine을 빌드
ReceiveChannel<>을 반환하여 해당 채널로부터 메시지를 전달받아 사용 가능
actor
정해진 채널로 메시지를 받아 처리하는 actor를 Coroutine으로 빌드
SendChannel<>을 반환하여 해당 채널의 send() 메소드를 통해서 actor에 새 메시지를 전송 가능
Suspend
suspend 함수가 호출될 경우 이전까지의 코드의 실행이 멈추며 suspend 함수가 처리가 완료된 후 멈춰있던 원래 Scope의 다음 코드가 실행
Coroutine이 멈출때 (대기시간 등) 코틀린 런타임은 해당 Coroutine이 실행되던 스레드에 다른 Coruotine을 할당하여 실행
그리고 멈춰있던 코루틴이 다시 실행될 때 사용 가능한 스레드에 할당
코루틴은 멈추면서 해당 루틴 상태를 저장하고 서브 루틴을 실행한 다음 저장한 부모루틴을 복원하는 방법으로 스레드에 영향을 주지 않는다.
suspend fun subRoutine(){
for(i in 0..10){
Log.d("subRoutine", "$i")
}
}
CoroutineScope(Dispatchers.Main).launch {
// 선 처리 코드
subRoutine()
// 후 처리 코드
}
위 코드에서 suspend 키워드를 사용했기 때문에 CoroutineScope안에서 자동으로 백그라운드 스레드처럼 동작
가장 큭 특징이 suspend 키워드를 붙인 함수가 실행되면 호출한 쪽의 코드를 잠시 멈추지만 스레드의 중단이 없기 때문
Suspend Fucntion
withContext
부모 Coroutine에 의해 사용되던 Context와 다른 Context에서 Coroutine을 실행 가능
기본적으로 부모의 Coroutine Dispatcher를 사용하지만 withContext로 Dispatcher를 달리 사용할 수 있는 것
ex) 호출 쪽 Coroutine은 Main Dispatcher로 UI를 제어, suspend 함수는 파일 io를 하는 경우 withContext를 사용하여 suspend 함수의 Dispatcher를 IO로 변경 사용할 수 있다.
CoroutineScope(Dispatchers.Main).launch {
// ui 처리
// ...
val result = withContext(Dispatchers.IO){
readFile()
}
Log.d("Coroutine", "$result")
}
withTixneout
코루틴이 정해진 시간 안에 실행되지 않으면 예외를 발생시키게 한다.
withTimeoutOrNull
코루틴이 정해진 시간 안에 실행되지 않으면 null을 결과로 돌려준다.
awaitAll
모든 작업의 성공을 기다린다. 작업 중 어느 하나가 예외로 실패하면 awaitAll도 그 예외로 실패한다.
joinAll
모든 작업이 끝날 때까지 현재 작업을 일시 중단한다.
상태 관리
join
사실 코루틴 내부에 여러 launch 블록이 있는 경우 모두 새로운 코루틴으로 분기되어 동시 실행되기 때문에 순서를 정할 수 없다.
순서를 정해야 한다면 join()을 사용해서 순차적으로 실행되도록 코드를 짤 수 있다.
CoroutineScope(Dispatchers.Default).launch {
launch {
for (i in 0..5){
delay(500)
Log.d("코루틴", "$i")
}
}.join()
launch {
for (i in 6..10){
delay(500)
Log.d("코루틴", "$i")
}
}
}
cancel
Coroutine의 동작을 멈추는 상태 관리 메소드로 하나의 Scope 안에 여러 Coroutine의 존재하는 경우 하위 Coroutine 또한 모두 멈춘다.
아래 코드에서 job을 캔슬하게되면 안에 있던 job1 도 중단된다.
delay()는 yield()와 마찬가지로 다른 코루틴에 실행을 양보하게 된다.
(정해준 시간이 끝날 때까지 무한 양보한다. 즉 양보했던 Coroutine이 다시 양보했더라도 delay 시간이 끝나지 않았다면 다시 양보했던 Coroutine에게 제어권이 넘겨진다.)
val job = CoroutineScope(Dispatchers.Default).launch {
val job1 = launch {
for (i in 0..10){
delay(500)
Log.d("코루틴", "$i")
}
}
}
binding.startBtn.setOnclickListener{
job.cancel()
}
Coroutine 실험
정지와 재개
fun startTest(view:View){
// startTest 는 suspend 함수가 아니므로 launch 빌더로 사용하여 Coroutine 시작
CoroutineScope.launch(Dispatchers.Main){ // Main Thread 를 사용 ( UI 는 정지 될까? )
testDelayTask() // UI 정지되지 않음
}
}
private suspend fun testDelayTask(){
Log.d("Coroutine","testDelayTask before")
delay(5000) // 이유는 delay 또한 suspend 함수로 delay 호출 시 testDelayTask() 가 suspend 되기 때문
Log.d("Coroutine","testDelayTask after")
}
위 코드에서 Button을 누르면 startTask 가 호출 될 때 UI가 정지할 것 같지만 멈추지 않는다.
이유는 delay 또한 suspend 함수이기 때문에 호출시 코틀린 런타임에 Coroutine으로 시작된다.
testDelayTask()가 suspend 되고 제어권은 다시 Main 스레드로 넘어가는 것이다.
그래서 UI의 멈춤이 없고 5초가 지나고 나서 resume 되어 다시 Coroutine이 제어권을 가져 after 로그를 출력한다.
결과 받기
private val testCoroutineScope = CoroutineScope(Dispatchers.Main)
fun startTest(view:View){
testCoroutineScope.launch(Dispatchers.Main){
binding.homeText.text = testDelayTaskAsync().await()
}
}
private suspend fun testDelayTaskAsync(): Deferred<String> =
testCoroutineScope.async {
Log.d("Coroutine","testDelayTask before")
delay(5000)
Log.d("Coroutine","testDelayTask after")
return@async "Finish"
}
testDelayTaskAsync() 함수가 Deferred 객체를 반환하도록 한다. Deferred 객체는 향후 언젠가 값을 제공한다는 의미이다.
해당 객체에 await() 을 호출하면 값이 반환될 때 코틀린 런타임에 전달해준다.
startTest() 는 suspend 함수가 아닌 클릭 리스너이기 때문에 testDelayTaskAsync() 함수에서 async 빌더를 사용해서 또 다른 Coroutine(Deferred 를 반환하는 Coroutine)을 시작하게 해야한다.
그러면 결과를 await 할 때 Main 스레드 중지가 아닌 백그라운드에서 작업을 수행할 수 있게 된다.
또는
private val testCoroutineScope = CoroutineScope(Dispatchers.Main)
fun startTest(view:View){
testCoroutineScope.launch(Dispatchers.Main){
binding.homeText.text = testDelayTaskAsync() //.await()
}
}
private suspend fun testDelayTaskAsync(): String =
//testCoroutineScope.async {
withContext(Dispatchers.Main){
Log.d("Coroutine","testDelayTask before")
delay(5000)
Log.d("Coroutine","testDelayTask after")
return@withContext "Finish"
}
withContext 로 async, Deferred, await() 호출을 대신할 수도 있다.
채널 통신
Channel 로 데이터 스트림을 비롯한 코틀린 간의 통신을 간단하게 구현할 수 있다.
send() 로 데이터를 전송하고 receive() 로 데이터를 수신한다.
val channels = Channel<Int>()
suspend fun channelTest(){
testCoroutineScope.launch(Dispatchers.Main){ channelTask1() }
testCoroutineScope.launch(Dispatchers.Main){ channelTask2() }
}
private suspend fun channelTask1(){
repeat((1..5).count()){
channels.send(it)
}
}
private suspend fun channelTask2(){
repeat(5){
Log.d("Channel Test", "Receive Channel Test ${channels.receive()}")
}
}
data 수신
다중 스레드 방식 보다 효율적인 코루틴을 사용해야겠다.
비동기 작업을 구조화된 방법으로 구현할 수 있고 더 간단하게 구현할 수 있다.
'Kotlin > Basic' 카테고리의 다른 글
object, companion object (0) | 2024.01.02 |
---|---|
Kotlin DSL, buildSrc 의존성 주입 (0) | 2023.12.07 |
Stream Fuction (0) | 2022.03.14 |
Closure, Scope Function (let, also, run, apply, with) (0) | 2022.03.11 |