Android

Android Coroutine - 제어 사용

이성진씨 2021. 4. 13. 19:28

01. Coroutine 제어

Coroutine 블록 내에서 사용되는 예약어들을 살펴보겠습니다.

해당 예약어를 이용하여 Coroutine 블록 내에서 값을 받거나 해당 Coroutine내에서 동기적으로 처리될지 비동기적으로 처리될지 구분하여 코드를 작성할 수 있습니다.

대표적인 예약어로 launch, async, Job, Deferred, runBlocking이 있습니다.

launch와 Job, async와 Deferred, runBlocking으로 묶어서 사용된 코드와 작성한 코드로 표시되는 출력값들을 이용해서 보여드리겠습니다.

 

02. launch와 Job

launch { } 를 이용하여 코루틴 블록내에서 다른 작업을 구분하는 경우 Job객체로 반환받을 수 있습니다.

Job객체는 코루틴 블록내에서 완료될 타이밍을 기다릴지 선택하거나 job객체들을 묶어 순서대로 호출되도록 할 수 있습니다.

                CoroutineScope(Dispatchers.Main).launch {
                    Log.i(TAG, "Dispatchers.Main - thread == "+Thread.currentThread().name)
                    val job1 = launch {
                        Log.i(TAG,"job1 start")
                        delay(2000)
                        Log.i(TAG, "job1 end")

                    }
                    val job2 = launch {
                        Log.i(TAG, "job2 start")
                        delay(4000)
                        Log.i(TAG, "job2 end")
                    }

                    Log.i(TAG,"pre joinAll")
                    joinAll(job1, job2)
                    Log.i(TAG,"post joinAll")
                    delay(3000)
                    Log.i(TAG,"last Log in coroutine")
                }
                
result ---
2021-04-13 17:58:31.253 ... I/ExecuteCoroutine: Dispatchers.Main - thread == main
2021-04-13 17:58:31.254 ... I/ExecuteCoroutine: pre joinAll
2021-04-13 17:58:31.255 ... I/ExecuteCoroutine: job1 start
2021-04-13 17:58:31.255 ... I/ExecuteCoroutine: job2 start
2021-04-13 17:58:33.259 ... I/ExecuteCoroutine: job1 end
2021-04-13 17:58:35.258 ... I/ExecuteCoroutine: job2 end
2021-04-13 17:58:35.259 ... I/ExecuteCoroutine: post joinAll
2021-04-13 17:58:38.264 ... I/ExecuteCoroutine: last Log in coroutine

 

Job을 이용하여 작업을 실행해보았습니다. CoroutineScope내에서 첫번째와 두번째작업을 launch시키고 해당 객체를 job1과 job2에 넣어주었습니다.  그 후  아래에서 joinAll이라는 함수를 이용하여 job1과 job2를 파라미터로 넘겨주었습니다. 이 대 coroutine내부에서는 job1과 job2의 작업이 완료될 때 까지 현재 coroutine안에서 대기하게 됩니다. 그리고 job1과 job2둘 다 작업이 완료되었을 때 "post joinAll"이라는 로그를 표시해주고 있습니다. 그 후 3초가 지나고나서 마지막 "last Log in coroutine"로그를 표시해주고 있습니다.

이렇게 하나의 코루틴안에 여러 작업들을 넣어두고 해당 작업이 다음 작업을 진행하기전에 완료되어야 하는 경우 사용할 수 있습니다.

 

Job을 이용해서 작업이 완료되는 타이밍을 잡을 수 있는게 확인되었습니다. 이제 job으로 작업을 취소시키는 방법을 알아보겠습니다.

		CoroutineScope(Dispatchers.Main).launch {
                    val job1 = launch {
                        repeat(10) {
                            yield()
                            Log.i(TAG,"job1 it == "+it)
                            delay(1000)
                        }
                    }

                    delay(3000)
                    job1.cancelAndJoin()
                    Log.i(TAG,"last Log in coroutine")
                }
result ---------------------
2021-04-13 18:19:17.649 ... I/ExecuteCoroutine: job1 it == 0
2021-04-13 18:19:18.651 ... I/ExecuteCoroutine: job1 it == 1
2021-04-13 18:19:19.655 ... I/ExecuteCoroutine: job1 it == 2
2021-04-13 18:19:20.659 ... I/ExecuteCoroutine: last Log in coroutine

Job은 하나만 등록되어 있고 작업에서는 0~9까지 1초단위로 반복문이 돌며 Log를 표시해주는 로직이 있습니다. 그리고 그 앞에는 yield()라는 새로 보는 함수가 있습니다. yield는 만약 job1에 중지 요청이 오게되면 현재 작업을 중지하라는 요청이 있을 경우 멈추는 중단점입니다. 아래 하단에서는 job1.cancelAndJoin을 통해서 job1을 종료시키고 있습니다. 이 때 yield가 없다면 중단되는 코루틴 내부에서는 작업이 중단되는 지점이 없어서 0~9까지 모두 작업을 돌게 될것입니다.

해당 코드의 출력문을 하단 result를 통해 확인할 수 있습니다.

 

03. async와 Deferred

async { } 를 이용해서 생성된 블럭은 Deferred라는 객체를 반환합니다. 따라서 해당 작업이 완료되길 기다렸다가 값을 반환받기 위해서는 Deferred객체에 await함수를 이용하여 받게됩니다.

                CoroutineScope(Dispatchers.Main).launch {
                    val deferred1 : Deferred<String> = async {
                        var msg: String = ""
                        repeat(5) {
                            delay(1000)
                            msg += it
                            Log.i(TAG, "deferred 1 - msg == $msg")
                        }
                        msg
                    }

                    val deferred2 : Deferred<String> = async {
                        var msg: String = ""
                        repeat(5) {
                            delay(1000)
                            msg = it.toString() + msg
                            Log.i(TAG, "deferred 2 - msg == $msg")
                        }
                        msg
                    }

                    val msg1 = deferred1.await()
                    val msg2 = deferred2.await()
                    Log.i(TAG, "msg1 == $msg1 // msg2 == $msg2")
                }
                
result ------------------
2021-04-13 18:38:55.589 ... I/ExecuteCoroutine: deferred 1 - msg == 0
2021-04-13 18:38:55.590 ... I/ExecuteCoroutine: deferred 2 - msg == 0
2021-04-13 18:38:56.592 ... I/ExecuteCoroutine: deferred 1 - msg == 01
2021-04-13 18:38:56.594 ... I/ExecuteCoroutine: deferred 2 - msg == 10
2021-04-13 18:38:57.595 ... I/ExecuteCoroutine: deferred 1 - msg == 012
2021-04-13 18:38:57.596 ... I/ExecuteCoroutine: deferred 2 - msg == 210
2021-04-13 18:38:58.597 ... I/ExecuteCoroutine: deferred 1 - msg == 0123
2021-04-13 18:38:58.598 ... I/ExecuteCoroutine: deferred 2 - msg == 3210
2021-04-13 18:38:59.600 ... I/ExecuteCoroutine: deferred 1 - msg == 01234
2021-04-13 18:38:59.605 ... I/ExecuteCoroutine: deferred 2 - msg == 43210
2021-04-13 18:38:59.606 ... I/ExecuteCoroutine: msg1 == 01234 // msg2 == 43210

현재 deferred1과 deferred2 각각 다른 문자열을 반환하고 있습니다. 그리고 await가 호출됨에 따라 deferred 작업이 완료될 때까지 기다렸다가 값을 반환받아서 사용할 수 있습니다.

현재 deferred1과 deferred2는 모두 반환값이 String으로 똑같은 상황이기 때문에 awaitAll(deferred1,deferred2)를 사용한다면 List<String>형태로 반환받을 수 있습니다.

 

앞의 Job과 같이 Deferred객체를 cancel시키면 어떻게 될지 궁금하여 추가적인 코드를 작성해보았습니다.

                CoroutineScope(Dispatchers.Main).launch {
                    val deferred1 : Deferred<String> = async {
                        var msg: String = ""
                        repeat(5) {
                            yield()
                            delay(1000)
                            msg += it
                            Log.i(TAG, "deferred 1 - msg == $msg")
                        }
                        msg
                    }
                    delay(4000)
                    deferred1.cancel()
                    Log.i(TAG,"deferred1 cancel after and deferred1.isCancelled == "+deferred1.isCancelled)
                    try {
                        val msg1 = deferred1.await()
                        Log.i(TAG, "msg1 == $msg1")
                    } catch (e: Exception) {
                        e.printStackTrace()
                    } finally {
                        Log.i(TAG,"try finally")
                    }
                }
result -----------------------
2021-04-13 18:59:15.381 ... I/ExecuteCoroutine: deferred 1 - msg == 0
2021-04-13 18:59:16.383 ... I/ExecuteCoroutine: deferred 1 - msg == 01
2021-04-13 18:59:17.385 ... I/ExecuteCoroutine: deferred 1 - msg == 012
2021-04-13 18:59:18.381 ... I/ExecuteCoroutine: deferred1 cancel after and deferred1.isCancelled == true
2021-04-13 18:59:18.384 ... W/System.err: kotlinx.coroutines.JobCancellationException: DeferredCoroutine was cancelled; job=DeferredCoroutine{Cancelled}@ca4a063
2021-04-13 18:59:18.384 ... I/ExecuteCoroutine: try finally

해당 Deferred객체는 하나로 cancel함수가 호출되고 나서 await를 호출하게되면 exception이 일어나게 됩니다. 여기서 객체의 isCancelled를 이용하여 현재 객체가 취소된 상황인지 아닌지 판별을 미리할 수 있습니다.

따라서 Job객체와 달리 Deferred객체의 경우 Cancel이 일어나게 된다면 exception발생으로 try catch문을 이용하여 묶어주어야합니다. 만약 try catch를 사용하지 않는다면 await를 호출하는 순간부터 아래 작업들은 모두 이루어지지 않게 됩니다.

 

04. runBlocking과 withTimeout

runBlocking의 경우 runBlocking안의 작업이 완료될 때까지 현재 thread를 멈추는 역할을 하게됩니다. 따라서 thread를 중단하는 함수에서 사용하기 위해 존재하고 있습니다. 코루틴안에서의 사용은 권장되지않고 있습니다.

 

                Log.i(TAG, "pre runBlocking")
                runBlocking {//이때동안 현재 runBlocking을 사용하는 Thread가 멈추게됨
                    Log.i(TAG,"in runBlocking")
                    delay(2000)
                    Log.i(TAG,"end runBlocking")
                }
                Log.i(TAG, "post runBlocking")
result ----------------
2021-04-13 19:14:29.127 ... I/ExecuteCoroutine: pre runBlocking
2021-04-13 19:14:29.128 ... I/ExecuteCoroutine: in runBlocking
2021-04-13 19:14:31.130 ... I/ExecuteCoroutine: end runBlocking
2021-04-13 19:14:31.131 ... I/ExecuteCoroutine: post runBlocking

 

위의 코드를 mainThread에서 호출시에 UI가 runBlocking만큼 중지되기 때문에 ANR을 발생시킬 위험이 있습니다.

 

                CoroutineScope(Dispatchers.Main).launch {

                    Log.i(TAG, "pre withTimeout")
                    withTimeout(3000) {
                        repeat(10) {
                            Log.i(TAG,"withTimeout it == "+it)
                            delay(1000)
                        }
                    }
                    Log.i(TAG, "post withTimeout")
                }
result ----------------------
2021-04-13 19:20:00.362 ... I/ExecuteCoroutine: pre withTimeout
2021-04-13 19:20:00.364 ... I/ExecuteCoroutine: withTimeout it == 0
2021-04-13 19:20:01.367 ... I/ExecuteCoroutine: withTimeout it == 1
2021-04-13 19:20:02.370 ... I/ExecuteCoroutine: withTimeout it == 2
2021-04-13 19:20:03.371 ... W/System.err: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 3000 ms
...
2021-04-13 19:20:03.374 ... I/ExecuteCoroutine: post withTimeout

withTimeout 함수를 사용한 상황입니다. 만약 API를 호출할 때 유용하게 쓰일 수 있는 함수로 해당 코드에서는 3초의 시간을 주고 해당 시간내에 완료가 되지 못하면 exception을 일으키며 내부 작업을 종료시킬 수 있습니다.

 

해당 포스트를 통해 코루틴의 추가적인 제어 함수들을 사용해보았습니다. 코루틴의 경우 Android의 구성요소에 맞춰서 동작범위를 제한할 수 있는 방법중 하나입니다. AAC와 함께 사용할 경우 ViewModelScope와 같은 기능또한 사용이 가능한데 현재 개발하고 계시는 환경에 맞춰 원하는 기능을 대부분 지원할 수 있을 것입니다.

 

포스트된 내용에 대해 미흡한 점이나 궁금증이 있으시다면 댓글로 알려주시면 답변드리겠습니다. 😀

 

참조할 수 있는 코드는 github에서 확인할 수 있습니다.

github.com/adwx13/CoroutineStudy.git

 

참조

thdev.tech/kotlin/2019/04/08/Init-Coroutines-Job/

medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%A0%9C%EC%96%B4-5132380dad7f