-
Improve app performance with Kotlin coroutines프로그래밍/Kotlin 2019. 11. 29. 00:37반응형
Coroutine 끄적끄적
Coroutine 무엇??
병렬성 디자인
freezing the add and blocking the main thread를 막는다
network나 disk operations를 main thread로부터 호출하는 것으로 막는다.
long-running tasks를 관리해보자
croutine은 두가지 동작만 알면 된다.
resume 는 현재 suspended됐던 부분부터 coroutine 실행을 다시 시작
suspend 는 현재 coroutine실행을 멈추고 모든 local variable저장
suspend는 suspend scope 안에서만 부르거나 launch 라는 coroutine builder에서 부르면 된다.
suspend fun fetchDocs() { // Dispatchers.Main val result = get("https://developer.android.com") // Dispatchers.IO for `get` show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
아래 예제에서는 main thread에서 coroutine을 시작하고
suspend fun fetchDocs() { // Dispatchers.Main
get()은 main thread 에서 돌게 되지만, get()을 호출하면서 현재 coroutine을 pause 한다. (main thread가 pause된다는 의미는 아니다.) network request가 완료 되면, get()은 main thread에 callback을 이용해 알려주시보다는 main thread에서 실행하던 coroutine을 resume한다.
Kotlin은 stack frame을 사용하는데, 이것은 어떤 function이 어떤 local variable을 사용하는지 관리하는 것이다. 만약에 coroutine이 suspending 됐을 때, Kotlin은 stack frame이라는 것으로 copy하고 나중을 위해 save 해놓는다. resume됐을 때, stack frame은 save해놨던 것을 다시 놀려놓고 그 function을 다시 실행시킨다. 마치 순서대로 sequential하게 blocking 하는 것처럼 보이지만 coroutine은 main thread를 blocking 하는 것은 아니다.
-main-safety 한 coroutine을 만들어보자.
Kotlin coroutine은 dispatcher라는 것을 사용한다. dispacher는 coroutine 실행을 어떤 thread에서 실행할지 결정하는 것을 의미한다. main thread 외부에서 코드를 동작하려면 Default나 IO dispatcher에서 실행하라고 말하면 된다. (이 것은 main thread에서 실행되더라도 해당된다.)
Kotlin에서 모든 coroutine은 무조건!!! 이 dispatcher안에서 동작해야한다. (이 것은 main thread에서 실행되더라도 해당된다.) Coroutine은 스스로 suspend되고 dispatcher는 스스로 resume하는 책임이있다.
Dispatchers.Main - coroutine을 main thread로 실행하고 싶으면 이 dispatcher를 사용하라. UI나 빨리 끝나는 work에 사용하라. suspend function을 호출하거나 UI변경, LiveData object 업데이트에 사용한다.
Dispatchers.IO - 이 dispatcher는 disk나 network I/O에 최적화돼있다. Room Component를 사용하거나 file을 읽거나 network work에 사용하라. (work thread)
Dispatchers.Default - 이 dispatcher는 CPU에서 동작해야하는 work에 최적화돼있다. sorting이나 JSON parsing에 사용하라. (work thread)
아래 예제를 다시 보면 get()은 IO thread에서 실행된다.
suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main }
그리고 withContext() block은 suspend function 그 자체여서 get() 도 suspend function이다. withContext()를 사용하면 line by line으로 thread사용을 control할 수 있는데, 이 말은 main thread에서 I/O작업을 실행하더라도 withContext()가 실행되어야할 thread의 실행을 보장해준다는 의미이다.
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main }
위 예제에서 fetchDocs()는 main thread에서 실행되더라도 get()을 호출했을 때 get()의 I/O작업은 I/O thread에서 동작하게 된다. coroutine이 suspend와 resume을 보장하기 때문에 withContext block의 작업이 끝나자마자 getchDocs는 get의 결과물로 main thread에서의 작업을 다시 시작한다.
** Key point : suspend 자체로는 worker thread에서의 실행을 보장하지 않는다. suspend function이 main thread에서 실행되는게 일반적이다. 그리고 suspend function을 main thread에서 호출되는 것도 일반적이다. 그래서 반드시!! 호출되는 thread를 보장하고 싶으면 suspend function 안에 withContext()를 호출해야한다. CPU에 최적화된 일이나 network, file I/O는 반드시 withContext()에서 호출한다.
withContext() performance -> 어렵CroutineScope 을 사용해보자
coroutine을 정의할 때, 반드시 CoroutineScope을 사용해야한다. CoroutineScope은 하나이상의 coroutine과 연관이 있는데, coroutine을 새로 시작하거나 이미 시작된 coroutine scope안에서 사용된다. dispatcher가 coroutine을 실행시킨다면 CoroutineScope은 coroutine을 실행시키진 않는다.
CoroutineScope의 가장 중요한 기능 중에 하나는 coroutine 실행을 멈추는 것인데 user가 app에 content를 남겼을때 coroutine 실행을 정확히 멈추는 것을 보장할 수 있다.
Android component에 CoroutineScope을 사용해보자. 안드로이드에서 CoroutineScope 구현을 component lifecycle에 맞출 수 있다. 이는 user가 더이상 사용하지 않는 activity나 fragments에서 memory leak을 방지할 수 있다. ViewModel처럼 component lifecycle에 맞춰져있다. 하지만 screen rotation 의 경우는 coroutine이 취소되지 않으니 걱정하지 않아도 된다.
C-Scope은 각 coroutine이 어디에서 시작하는지 알고 있다. 즉, 어디서든 cancel할 수 있다는 의미이다. Scopes은 스스로 coroutine을 전염시킨다. 한 coroutine이 다른 coroutine에서 시작하면 같은 scope에서 시작한거로 친다. 내가 시작한 coroutine에서 다른 lib의 coroutine이 시작했으면 내가 언제든지 취소할 수 있다는 의미이다.
예를들면, ViewModel안에서 coroutine을 시작했으면 중요한 의미를 갖는다. 내가 만든 ViewModel이 종료되면 유저가 스크린을 떠났다는 의미이기 때문에 memory leak을 막기 위해 모든 비동기 작업들은 멈춰야할 것이다. 단, ViewModel이 종료되더라도 계속되어야하는 작업이라면 component 보다 아래 layer에서 작업을 시작해야 할 것이다.
coroutine은 취소되면 CancelException을 던진다. Exception handling을 해야할 것이다. viewModelScope이라고 KTX library에 extension function이 있다. 이거로 coroutine이 ViewModel이 종료되면 같이 종료되게 할 수 있다.
coroutine을 시작해보자.
launch와 async만 쓰면 실행할 수 있다.
-launch: 새로운 coroutine 을 시작하고 caller에게 result를 전달하지 않는다. “fire and forget“ 되는 일에 이용하자.
-async: 새로운 coroutine을 시작하고 caller에게 result를 전달한다. 반드시 await를 호출할 수 있는 suspend function과 함께 쓴다.
launch는 일반적인 function에 사용된다. async는 다른 coroutine이나 suspend function 에서 호출하자.
이전 예제와 같이 viewModelScope을 사용해서 launch를 호출해보자.
fun onDocsNeeded() { viewModelScope.launch { // Dispatchers.Main fetchDocs() // Dispatchers.Main (suspend function call) } }
suspend function에 의해서 시작되는 모든 coroutine은 다시 caller로 돌아오기전에 종료되도록 보장되어야한다. 그래서 CoroutineScope에서 coroutine을 하나 이상 실행하면, awain()나 awaitAll()을 걸어야한다.
await()를 아래처럼 각각 걸거나
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
awaitAll()로 한방에 걸수도 있다.
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) coroutineScope { val deferreds = listOf( // fetch two docs at the same time async { fetchDoc(1) }, // async returns a result for the first doc async { fetchDoc(2) } // async returns a result for the second doc ) deferreds.awaitAll() // use awaitAll to wait for both network requests }
awaitAll()을 호출하지 않으면 다시 caller를 resume하지 못한다. 그런데 coroutineScope은 awaitAll()을 호출하지 않았어도 coroutine이 끝나길 기다린다.(?) coroutineScope은 모든 Exception을 잡는다.
반응형'프로그래밍 > Kotlin' 카테고리의 다른 글
Kotlin multiplatform 프로젝트를 생성해보자 (0) 2020.09.01 Lessons learnt using Coroutines Flow in the Android Dev Summit 2019 app (0) 2020.01.29 Kotlin by keyword를 이용해 상속대신 Delegation을 해보자 (0) 2019.09.03 Kotlin Coroutine Basic (0) 2018.11.04 코틀린 - 함수 정의와 호출 (0) 2018.01.03