Android

1. Kotlin Coroutine - suspend, resume, Dispatchers.

Sara.H 2024. 5. 15. 16:21

https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb

 

코루틴의 개념의 첫 구현체는 Simula (1967) 

 

Android 에서 코루틴의 역할은 뭘까? 

1. Main thread 를 블럭하는 긴 태스크 (long-running task) 처리 

2. Main-safety 는 어떤 suspend function 이든 main thread 에서 호출될 수 있도록 함

 

suspend 키워드는 코틀린 컴파일러에 특수한 처리를 가하기 위한 용도. 

suspend function 은 일반 함수와 달리 두 가지 연산이 더 있음. 

일반 함수에서 제공하는 invoke (or call), return + suspend, resume 

 

suspend - 현재 코루틴의 실행을 멈추고, 지역 변수들을 저장함

resume - 지연된 코루틴을 멈췄던 곳에서부터 재개함

 

suspend, resume 은 콜백을 대체하기 위해 함께 사용된다. 

콜백을 어떻게 대체하는가? 

https://miro.medium.com/v2/resize:fit:1400/format:webp/1*U24_ZyMJKI_c2qMspCXxZw.gif

suspend fun fetchDocs() {
	val docs = get("...") // get 은 suspend 함수, Dispatchers.IO 사용 
	show(docs)
}

 

메인 스레드 스택 구조 - 위 GIF 를 순차적으로 따라가면서 어떤 연산을 수행하는지 확인해보자. 

 

1. suspend 키워드가 부착된 fetchDocs 함수가 스택에 올라간다 (invoke) 

2. suspend 이므로 스택에서 잠시 제거된 후 재개를 기다린다 (suspend) - 스택에서 제거된다는 표현 보다는, 현재 스택 프레임이 복사된 채로 어딘가에 저장됨. 

3. get 이 스택에 올라간다 (invoke) 

4. get 또한 suspend 이므로 잠시 스택에서 제거된 후 재개를 기다린다 (suspend

--- 메인 스레드 위에 있는 모든 코루틴들이 suspend 상태일때 메인 스레드는 free 하다 

5. get 작업이 끝난 후 메인 스택에 다시 올라간다 (resume) - 복사했던 스택 프레임이 다시 올라감 

6. get 의 결과값이 반환되고 스택에서 제거된다 (return) 

7. fetchDocs 가 스택에 다시 올라가고 실행을 재개한다 (resume) 

8. show(docs) 까지 실행을 마친 후 스택에서 제거된다 (return) 

 

코루틴은 메인 스레드에서 실행되며, suspend 는 백그라운드 실행을 의미하지 않는다.

 

코틀린에서는 모든 코루틴이 반드시 Dispatcher 위에서 실행되어야 한다. - main thread 에서 실행 되더라도 마찬가지다. 

 

코루틴은 자기 자신을 suspend 할 수 있다. 

Dispatcher 가 바로 suspend 된 코루틴을 resume 하는 방법을 아는 애다. 

 

코루틴이 어디서 실행되어야 할지 명시하기 위해 코틀린은 세 가지 Dispatcher 를 제공한다. 

1. Dispatchers.Main 

- suspend function 을 호출할때 

- UI 함수를 호출할 때 

- LiveData 를 업데이트 할 때 

 

2. Dispatchers.IO 

디스크, 네트워크 요청 

 

3. Dispatchers.Default 

CPU 집중적인 작업을 메인스레드 밖에서 할때 

- 리스트를 정렬한다 

- JSON parsing 

- DiffUtils

 

Room, Retrofit, Volley 등 라이브러리들은 내부적으로 알아서 main-safety 를 제공하고 있으므로 개발자가 직접 이를 신경써 줄 필요는 없다. 

 

line by line 으로 어떤 Dispatcher 위에서 코루틴이 수행되는지 확인해본다. 

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

suspend fun fetchDocs() {
    val result = get("something") // Dispatchers.Main
    show(result) // Dispatchers.Main 
}

fun show(result : Any) {
    println("show something please...")
}

suspend fun get(url: String) // Dispatchers.Main
= withContext(Dispatchers.IO) {
    // Dispatchers.IO
    // do something IO intensive
} // Dispatchers.Main

 

코루틴을 쓰면 thread dispatch 를 보다 미세한 수준까지 할 수 있다. 

Main 뿐만 아니라 다른 Dispatcher 에서 수행되더라도 안전한 함수인지 보장해주기 위해 withContext 를 사용하는게 좋다. 

 

fetchDocs 는 Main thread 에서 실행되지만, 안전하게 get 을 호출해서 백그라운드에서 요청을 수행한다. 

코루틴은 suspend 와 resume 을 지원하기 때문에, 메인 스레드에서 실행되는 코루틴은 withContext 블럭 수행이 끝난 후 그 결과와 함께 resume 가능하다. 

 

잘 짜여진 suspend function 은 항상 main-safe하다 (메인 스레드에서 호출되어도 괜찮다) - Dispatcher 잘 넣어주자. 

 

withContext 의 성능? 

콜백 혹은 rx 만큼 빠르다. 

database 라이브러리 내부적으로 어떤 요청에 대해서 withContext(Dispatchers.IO) 를 하고 있더라도, outer withContext 로 한번 감싸주면 요청이 10번 가더라도 문맥 전환은 한번이면 된다. -? 여러번의 IO 콜이 있더라도, 하나의 동일한 코루틴 컨택스트 안에서 이루어진다 - ? 오버헤드가 최소화 될 수 있다. 

Default 디스패처와 IO 디스패처 사이를 오가는 것 또한 스레드 변경을 피하도록 최적화 되어 있다. 

import kotlinx.coroutines.*

// Simulated database access function
suspend fun fetchFromDatabase(query: String): List<String> {
    delay(100) // Simulate database access delay
    return listOf("Data for $query")
}

// Function to fetch multiple data sets
suspend fun fetchDataSets(): List<List<String>> {
    return withContext(Dispatchers.IO) { // Switch to IO dispatcher once
        val dataSet1 = fetchFromDatabase("SELECT * FROM users")
        val dataSet2 = fetchFromDatabase("SELECT * FROM orders")
        val dataSet3 = fetchFromDatabase("SELECT * FROM products")
        listOf(dataSet1, dataSetSet2, dataSet3) // Collect and return all data
    }
}

// Main function to run the coroutine
fun main() = runBlocking {
    val data = fetchDataSets()
    data.forEach { dataSet ->
        println(dataSet)
    }
}

by GPT-4