ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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 잡는다. 

     

     

    https://developer.android.com/kotlin/coroutines

    반응형
Designed by Tistory.