Search

'coroutine'에 해당되는 글 2건

  1. 2021.05.21 Kotlin - Coroutine
  2. 2020.04.15 Kotlin - Coroutine

Kotlin - Coroutine

개발/안드로이드 2021. 5. 21. 20:00 Posted by 아는 개발자

Coroutine을 공부할 때 당장 실행되는 코드를 짜려고 launch, async 함수부터 먼저 써보게 되는데(과거의 나) 이것보단 Coroutine을 이루는 구조가 무엇인지를 먼저 공부하고 유틸리티 함수를 사용하면 훨씬 이해하기가 쉽다.  Coroutine을 이루는 구조는 크게 CoroutineScope과 CoroutineContext다. 아래 그림으로 보면 CoroutineScope이 CoroutineContext를 포함하는 관계다.

 

 

1. CoroutineScope

 

CoroutineScope은 Coroutine이 활동할 수 있는 범위를 말한다. 예를 들어 Coroutine이 ViewModel의 생성주기 내에서만 동작하게 할 수 있고 Activity Lifecyle 생명주기를 따라서 동작하게 할 수 있는데 CoroutineScope은 Coroutine의 활동범위를 말한다. 이 속성을 잘 이용하면 Component의 생성주기에 맞춰 Coroutine 작업을 자동으로 취소할 수 있어서 유용하다. Kotlin에서는 안드로이드에서 사용할 수 있도록 몇가지 CoroutineScope을 미리 만들어뒀다. 

 

  • GlobalScope: 앱 프로세스의 생명주기를 따라감. 
  • MainScope: UI 관련 작업을 처리하는 용도.
  • ViewmodelScope: ViewModel의 생성주기를 따라감.
  • LifecycleScope: Activity, Fragment의 생명주기를 따라감. 생명주기별로 콜백이 다르다.
MainScope().launch {}

GlobalScope.launch {}

 

CoroutineScope 인터페이스를 구현해서 커스텀한 CoroutineScope을 만들수도 있긴 한데 공식 문서에서 이 방법은 추천하진 않고 있다.

 

2. CoroutineContext 

 

CoroutineContext는 Coroutine을 이루는 정보다. Coroutine 이름, Job, Dispatcher, ExceptionHandler 가 이에 해당한다. Dispatcher는 Coroutine이 실행될 쓰레드 풀을 의미한다. 대표적으로 Main, IO 쓰레드 풀이 있어서 UI 작업의 경우에는 Main, 디스크 작업에는 IO 쓰레드를 사용하도록 지정 할 수 있다. Executors 라이브러리를 이용해 커스텀으로 만든 쓰레드 풀에도 지정이 가능하다. 아래 코드는 우선순위가 높은 쓰레드 풀에서 동작하는 CoroutineScope을 만든 예다.

 

val customExecutor: Executor = Executors.newCachedThreadPool { r ->
    Thread(r, "CustomThread").apply {
        priority = Thread.MIN_PRIORITY
    }
}
val customDispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        customExecutor.execute(block)
    }
}

CoroutineScope(customDispatcher).launch {

}

 

ExceptionHandler는 Coroutine 내의 코드 실행중 발생하는 Exception을 처리할 수 있는 Handler다. 현재 Scope 별로 Exception Handler를 다르게 둘 수 있기 때문에 이것도 잘 써먹으면 유용하다. 

 

val handler = CoroutineExceptionHandler { context, th->
    println("$context ${th.toString()} ")
}

GlobalScope.launch(handler) {
    val async1 = async(){ 1 }
}

 

3. Utility 함수 

 

3.1 launch

 

launch 함수는 CoroutineScope내에서 실행되며, 현재 쓰레드를 막지 않고(blocking) 동작할 수 있는 새로운 Coroutine Job을 생성한다. 병렬로 수행되기 때문에 여러가지 작업을 동시에 수행할 때 쓰면 좋다. 아래 코드는 GlobalScope 내에서 두개의 Coroutine Job을 생성한 코드다. 앞에 코드에 300ms 의 딜레이를 줬다. 그 결과 scope2가 먼저 프린트 되고, scope1은 그 이후에 프린트 된다.

 

GlobalScope.launch {
    launch {
        delay(300)
        println("scope1")
    }
    launch {
        println("scope2")
    }
}

2021-05-21 16:14:17.677 I/System.out: scope2
2021-05-21 16:14:17.978 I/System.out: scope1

 

Job 내부 함수인 join() 은 동시성을 제어할 수 있는 함수다. 현재 Coroutine의 실행이 종료되지 않을 때까지 다음 코드를 실행하지 않는다. 순서를 관리할 때는 이 함수를 쓰면 된다. 그리고 cancel() 처럼 취소할 수 있는 함수도 있다. 이건 실제로 사용하다 보면 어떻게 써야하는지 감이 온다.

 

3.2 async 

 

async 함수는 launch 와 거의 동일하고 결과 값을 받을 수 있다는 점이 추가 됐다. 아래 코드의 두 async Job은 각각 1, 2를 리턴하는 CoroutineScope이다. async 내부 await() 함수는 여기서 실행된 결과 값을 받아오게 된다. 여기서 주의깊게 볼 부분은 각각에 delay를 300ms, 100ms 씩 줬는데도 start 로그로부터 결과 값까지 걸린 시간은 둘의 합인 400ms가 아니라 가장 긴 delay인 300ms라는 점이다. 두 Job을 병렬로 처리했기 때문에 가장 delay를 오래 잡는 Job의 시간만큼 소요된다.

 

GlobalScope.launch {
    println("start")
    val a = async {
        delay(300)
        1
    }
    val b = async {
        delay(100)
        2
    }

    println("a + b = ${a.await() + b.await()}")
}

2021-05-21 16:22:19.885 I/System.out: start
2021-05-21 16:22:20.190 I/System.out: a + b = 3

 

3.3 withContext 

 

동일한 CoroutineScope 내에서도 종종 Coroutine Context를 바꿔야 할 일이 생긴다. 예를 들면 I/O 작업을 수행 중에도 중간중간 화면 UI를 업데이트 해야하는 경우 Dispatcher를 바꿔 코드에 적용하는 쓰레드를 변경해야한다. 이럴때 쓰면 유용한 함수가 withContext다. 아래 코드는 I/O 스레드 풀에서 "abc", "def"라는 문자열을 받아오고 두 문자를 합해서 text라는 변수를 만들었다. UI 에 적용하려면 withContext를 이용해 임시로 Main함수로 바꿔주어 UI 컴포넌트에 접근 할 수 있다. 참고로 withContext는 내부적으로 async{}.await()로 구현돼 있어 내부 코드가 모두 실행된 다음에 다음 코드로 넘어가게된다. 

 

CoroutineScope(Dispatchers.IO).launch {
    val a = async {
        "abc"
    }
    val b = async {
        "def"
    }
    
    val text = a.await() + b.await()
    
    withContext(Dispatchers.Main) {
        textView.text = text
    }
}
728x90

Kotlin - Coroutine

개발/안드로이드 2020. 4. 15. 22:47 Posted by 아는 개발자

Kotlin의 Coroutine은 비동기 작업을 지원하는 "lightweight threads" 인 컴포넌트다. 이미 안드로이드에 있는 AsyncTask와 비슷한 역할을 수행하지만 Coroutine은 특별한 오버라이드 함 수 없이 간단하게 구현이 가능하고 깊게 들어가면 세부 동작 방식과 구현 철학은 다르다. 이번 포스트에서는 Kotlin의 Coroutine에 대해서 전반적인 소개와 사용 방법을 소개하려고 한다.

 

 

 

먼저 Coroutine은 하나의 task가 아니라, 여러 개 순서가 정해진 sub-tasks의 집합이다. Coroutine에서는 여러 개의 sub-task가 존재하는데 이들의 실행 순서는 보장된(guaranteed) 순서로 실행이 된다. 즉 코드 상에서는 얘네들이 sequential 하게 짜여져 있어도 코드를 어떻게 짜느냐에 따라서 실행 순서는 다를 수 있다. 이게 무슨 뚱딴지 같은 소리인가 싶을 수도 있지만 이건 asynchronous 작업을 좀더 유연하게 지원하기 위한 철학 정도로 이해하면 될 것 같다. 글로 보면 이해가 잘 되지 않을 수 있는데 예제 코드를 본다면 감이 어느정도 잡힐 것이다.

 

0. Quick Example

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }
    async { withContext(this.coroutineContext) { printLog("2") } }
    async { withContext(this.coroutineContext) { printLog("3") } }
}

 

안드로이드에서 Coroutine을 사용한 예제 코드다. 아직 Coroutine에 대해서 잘 모르더라도 느낌상으로는 로그를 찍는 함수 호출로 미뤄 볼 때 로그는 1 -> 2 -> 3 의 순서로 찍혀야 할 것 같다. 하지만 실제 출력 결과 매번 실행할 때마다 순서가 다르게 나온다.

 

2020-04-15 20:38:06.190 24595-24791/kwony.study D/CoroutineSample: 1
2020-04-15 20:38:06.191 24595-24790/kwony.study D/CoroutineSample: 3
2020-04-15 20:38:06.191 24595-24791/kwony.study D/CoroutineSample: 2

 

그런데 이번에는 로그 출력용 객체에 await 함수를 붙여주면

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }.await()
    async { withContext(this.coroutineContext) { printLog("2") } }.await()
    async { withContext(this.coroutineContext) { printLog("3") } }.await()
}

 

 이렇게 결과가 달라지고 값도 고정되게 나온다.

 

2020-04-15 20:40:12.687 25004-25078/kwony.study D/CoroutineSample: 1
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 2
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 3

 

Coroutine을 사용하면 작업의 순서를 요리조리 조정할 수 있다. 맛보기로 이정도면 좋을 것 같다.

 

1. Coroutine Builder 

 

Coroutine을 사용하려면 Coroutine 객체를 생성하는 작업을 먼저해야한다. Coroutine Builder는 Coroutine을 생성하기 위한 팩토리 함수다. Kotlin에서는 Coroutine을 생성하기위한 기본 빌더 함수를 제공한다.

 

runBlocking

 

현재 시작중인 thread를 block 시키고 Coroutine 작업을 시작하게 하는 Coroutine 빌더다. Main Thread에서 이 함수가 호출 됐으면 UI가 잠시 멈추고 coroutine 함수가 실행된다. 테스트를 위해 onCreate 함수에서 runBlocking Coroutine을 생성하고 3초간 딜레이를 주었다. 그 결과 화면이 초기화되는 시간이 3초간 딜레이되는 현상이 발생한다.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    runBlocking {
        delay(3000)
    }

 

synchronous하게 처리해야하는 작업에 사용되는데 현재 실행중인 thread를 block시키는 문제가 꽤 골치 아프기 때문에 가능하면 쓰지 않을 것을 추천하는 문서도 있다. 정말로 필요한 경우가 아니면 가급적 사용하지 않는 것이 좋을 수도.

 

GlobalScope 

 

runBlocking 빌더와 달리 현재 실행중인 Thread를 block 시키지 않고 백그라운드에서 작업을 실행하는 Coroutine이다. 최상위 Coroutine을 생성 할 때 사용하는데 이 Coroutine은 애플리케이션과 동일한 생명주기를 갖게 된다. 이에 대한 자세한 내용은 CoroutineScope 에서 소개하는 것이 좋을 것 같다. 예제 코드를 한번 보자.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    GlobalScope.launch {
        delay(3000)
        Log.d(coroutineSampleTag(), "World")
    }
    Log.d(coroutineSampleTag(), "Hello")
}

 

GlobalScope 으로 만들어진 Coroutine 에서는 3초간 딜레이를 주고 World를 출력하고 생성 다음에는 Hello를 출력하는 코드다. 실행 결과는 아래와 같다. Hello가 먼저 출력되고 Coroutine 내부의 World 문자열은 3초뒤에 출력되는 것을 확인 할 수 있다.

 

2020-04-15 21:21:45.224 30001-30001/kwony.study D/CoroutineSample: Hello
2020-04-15 21:21:48.234 30001-30069/kwony.study D/CoroutineSample: World

 

CoroutineScope(context: CoroutineContext)

 

Coroutine이 동작할 context를 지정해서 Coroutine을 만드는 함수다. 저기 안에 인자에 특정 CoroutineContext를 넣을 수 있는데, 이에 대해서 자세하게 설명하는 것보다는 어떤 Thread에서 동작할 것인지 지정하는 것 정도로만 보면 좋을 것 같다. 대표적으로 Dispatchers.Main과 Dispatchers.IO가 있다.

 

CoroutineScope(Dispatchers.Main).launch {}
CoroutineScope(Dispatchers.IO).launch {}

 

Dispatchers.Main으로 넣은 경우는 해당 Coroutine 작업이 MainThread 즉 UI를 관장하는 Thread에서 돌아가도록 선언한 것이고 Dispatchers.IO인 경우에는 Blocking IO 용 공유 쓰레드 풀에서 동작하도록 지정한 것이다. RxJava에서 subscribeOn 함수에 넣은 값들과 유사한 개념이다. 이 Builder 함수들도 GlobalScope 처럼 진행중인 Thread를 block 하지 않고 동작한다.

 

2. async { }

 

Coroutine 초기 예제에서도 확인 했던 것 처럼 Coroutine 작업들은 비동기적으로 처리하는 것이 가능하다. 비동기로 처리하려는 Coroutine들은 async { } 로 작업을 선언하면 된다.

 

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async {
        delay(3000)
        printLog("1")
        return@async 1
    }
    val deferred2 = async {
        delay(3000)
        printLog("2")
        return@async 2
    }
    val deferred3 = async {
        delay(3000)
        printLog("3")
        return@async 3
    }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}

 

예제에서 확장해서 async Coroutine에 딜레이 3초와 리턴값을 넣고 마지막에 각 Coroutine 결과의 총합을 출력하는 코드를 추가했다.

 

2020-04-15 22:18:29.090 3877-3990/? D/CoroutineSample: Coroutine Start
2020-04-15 22:18:32.108 3877-3996/kwony.study D/CoroutineSample: 2
2020-04-15 22:18:32.109 3877-3993/kwony.study D/CoroutineSample: 3
2020-04-15 22:18:32.112 3877-3991/kwony.study D/CoroutineSample: 1
2020-04-15 22:18:32.125 3877-3996/kwony.study D/CoroutineSample: total sum: 6

 

Coroutine 의 로그 출력 순서는 실행할 때마다 변경되는 반면 마지막에 Coroutine 반환 값의 합을 출력하는 부분은 항상 마지막에 출력되는데 이는 각 async 객체에 await() 함수를 사용해서 리턴값을 반환했기 때문이다. await() 함수는 async 함수가 모두 종료될 때 까지 구문 실행을 기다리도록 하는 함수다. 합을 출력하는 부분은 세개의 Coroutine의 연산결과를 모두 기다려야하기 때문에 제일 마지막에 실행 될 수 밖에 없다.

 

또 주목할 만한 점은 모든 Coroutine에 3초씩 딜레이를 주었는데 각 Coroutine의 로그 출력 시간은 3초씩 차이가 나지 않는다. 이는 각각의 Coroutine이 Sequential하게 동작하지 않고 Parallel하게 동작했기 때문이다. Coroutine의 로그 출력 순서가 바뀌는 이유도 Parallel 하게 동작했기 때문이다.

 

3. suspend fun method() = coroutineScope { }

 

Kotlin 함수 선언 앞에 suspend와 coroutineScope 으로 body를 만들어주면 Coroutine 에서 실행가능한 함수로 선언 해줄 수 있다. 아래 코드는 앞서 예제로 소개한 코드랑 동일한 로직으로 동작하며 출력되는 결과도 동일하다 (Coroutine 로그 출력 순서만 빼면)

 

private suspend fun coroutineMsg(msg: String, ret: Int): Int = coroutineScope {
    delay(3000)
    printLog(msg)
    return@coroutineScope ret
}

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async { coroutineMsg("1", 1) }
    val deferred2 = async { coroutineMsg("2", 2) }
    val deferred3 = async { coroutineMsg("3", 3) }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}
2020-04-15 22:42:07.182 7959-8043/kwony.study D/CoroutineSample: Coroutine Start
2020-04-15 22:42:10.197 7959-8045/kwony.study D/CoroutineSample: 1
2020-04-15 22:42:10.197 7959-8048/kwony.study D/CoroutineSample: 3
2020-04-15 22:42:10.198 7959-8043/kwony.study D/CoroutineSample: 2
2020-04-15 22:42:10.217 7959-8044/kwony.study D/CoroutineSample: total sum: 6

 

728x90

'개발 > 안드로이드' 카테고리의 다른 글

Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (1) 2020.04.06