반갑습니다!

[Kotlin] Coroutine Context and Dispatchers 본문

Kotlin

[Kotlin] Coroutine Context and Dispatchers

김덜덜이 2020. 7. 5. 15:33

코루틴에는 Coroutine Context와 Dispatcher라는 것이 있다. 이번엔 그것들에 대해서 알아보자.

Dispatchers and Threads

코루틴은 기본적으로 Coroutine Context 실행되는데, Context 요소 중에는 Dispatcher가 있다. 그리고 이 Dispatcher라는 것은 코루틴이 어떤 쓰레드에서 실행될지를 결정해준다. runBlocking, launch , async  등의 코루틴 빌더 함수에는 Courinte Context를 매개변수로 전달해줄 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch { // main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined
        println("Unconfined: I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) {
        println("Default: I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) {
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
}

/* 실행결과
Unconfined: I'm working in thread main
Default: I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking: I'm working in thread main
*/

이처럼 어떤 값을 매개변수로 전달하는지에 따라서 실행되는 매 변수가 달라진다. 매개변수로 아무런 값도 전달하지 않는다면 자신을 호출한 코루틴 스코프의 context를 상속받아 작업을 하게 된다. 즉, 위의 예제에서는 `runBlocking 과 같은 coroutine context에서 실행되므로 메인쓰레드에서 실행된다.

주의할 점으로는 newSingleThreadContext는 새로운 쓰레드를 생성해서 코루틴을 동작시키기 때문에 메모리 릭이 발생할 가능성이 있다.

newSingleThreadContext("MyOwnThread").use {
    launch(it) {
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
}

다음과 같이 자동으로 자원을 반납해주는use() 를 사용해서 작성하는 것이 좋다.

Debugging coroutines and threads

코루틴은 쓰레드를 넘나들거나 비동기 작업을 하는 등 디버깅하기가 어렵다. 어떤 코루틴에서 실행된 것인지 알기 위해서는 JVM에 -Dkotlinx.coroutines.debug 옵션을 주면 된다.

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")    
}

/* 실행결과
[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42
*/

Jumping between threads

코루틴은 withContext() 를 사용해서 context의 전환을 쉽게 할 수 있다.

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }    
}

/* 실행결과
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
*/

위 예제에서는 context의 전환뿐만 아니라 newSingleThreadContext()use() 를 같이 사용했다는 것을 볼 수 있다.

Job in the context

코루틴에서 Job 객체는 context의 일부이다. 이번엔 Job 객체를 확인해보자.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")

    launch {
        println("My job is ${coroutineContext[Job]}")
    }

    async {
        println("My job is ${coroutineContext[Job]}")
    }
}

/* 실행결과
My job is BlockingCoroutine{Active}@579bb367 [main]
My job is StandaloneCoroutine{Active}@7ab2bfe1  [main]     
My job is DeferredCoroutine{Active}@497470ed [main]     
*/

이처럼 coroutineContext에는 직접 접근할 수 있다.

Children of a coroutine

코루틴 스코프 안에서 코루틴이 실행되면 그 코루틴은 부모 코루틴의 자식이 된다. 이 때 GlobalScope 는 어플리케이션 전체에서 실행되는 코루틴이기 때문에 예외가 된다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val request = launch {
        GlobalScope.launch {
            println("job1: I run in GlobalScope and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }

        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel()
    delay(1000) // 어떤 일이 일어나는지 확인하기 위해 딜레이를 걸어줌
    println("main: Who has survived request cancellation?")
}

/* 실행결과
job1: I run in GlobalScope and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?
*/

위의 실행결과를 확인하면 부모 코루틴이 취소되었기 때문에 자식 코루틴 역시 취소되었다는 것을 볼 수 있다. 이 때 GlobalScope 로 선언된 코루틴은 독립적으로 실행되는 것을 알 수 있다.

Parental responsibilities

부모 코루틴은 join() 을 사용하지 않아도 모든 자식 코루틴이 모두 끝날 때까지 기다린다. 예제를 보면서 확인해보자.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // 자식 코루틴 3개 생성
            launch  {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // 자식 코루틴을 포함해서 request가 끝나길 기다린다
    println("Now processing of the request is complete")
}

/* 실행결과
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete
*/

Combining context elements

코루틴을 사용하다보면 coroutineContext에 여러 개의 요소들을 넘겨주고 싶을 수 있을 것이다. 이럴 때는 미리 overriding 되어있는 + 연산자를 사용해서 전달해주면 된다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }
}

/* 실행결과
I'm working in thread DefaultDispatcher-worker-1 [DefaultDispatcher-worker-1]
*/

Coroutine scope

예를 들어, 화면 UI를 담당하는 부분에서 코루틴을 사용한다고 가정해보자. 사용자가 해당 화면을 벗어나면 코루틴은 모두 종료되어야한다. 이 때 코루틴이 여러 개가 실행 중이었다면 모두 cancel() 해줘야하기 때문에 번거로울 수 있다. 이는 Coroutine Scope를 활용하여 쉽게 해결할 수 있다.

import kotlinx.coroutines.*

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default)

    fun destory() {
        mainScope.cancel()
    }

    fun doSomething() {
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L)
                println("Coroutine $i is done")
            }
        }
    }
}

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.doSomething()
    println("Launched coroutines")
    delay(500L)
    println("Destroying activity!")
    activity.destroy()
    delay(1000)
}

/* 실행결과
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
*/

Activity 클래스에서 작업하는 모든 코루틴은 모두 mainScope 안에서 생성했다. 그렇게 해서 mainScope.cancel() 을 실행하면 모든 코루틴을 쉽게 종료시킬 수 있다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Principle of Coroutine  (0) 2020.07.05
[Kotlin] Composing Suspending Functions  (0) 2020.07.05
[Kotlin] Cancellation and Timeouts  (0) 2020.07.04
[Kotlin] Coroutine Basics  (0) 2020.07.04
[Kotlin] Coroutine Guide  (0) 2020.07.04