본문 바로가기
Kotlin

[Kotlin] Kotlin 공식 문서 번역 - 코루틴 문맥과 디스패처 (Coroutine context and dispatchers)

by 노력남자 2023. 10. 1.
반응형

코루틴 문맥과 디스패처 (Coroutine context and dispatchers)

 

코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext 유형의 값으로 표시되는 컨텍스트에서 실행됩니다.

코루틴 컨텍스트는 여러 요소의 집합입니다. 주요 요소는 코루틴의 Job(이전에 보았던 것)과 이 섹션에서 다루는 디스패처(Dispatcher)입니다.


디스패처와 스레드


코루틴 컨텍스트에는 코루틴의 실행에 사용되는 스레드 또는 스레드 집합을 결정하는 코루틴 디스패처(see CoroutineDispatcher)가 포함되어 있습니다. 코루틴 디스패처는 코루틴 실행을 특정 스레드에 제한하거나 스레드 풀에 디스패치하거나 무제한으로 실행하도록 할 수 있습니다.

launch 및 async와 같은 모든 코루틴 빌더는 새로운 코루틴 및 다른 컨텍스트 요소에 대한 디스패처를 명시적으로 지정하는 데 사용할 수 있는 선택적인 CoroutineContext 매개변수를 허용합니다.

다음 예를 시도해보세요:

launch { // 부모 코루틴인 주 실행 블록의 컨텍스트
    println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // 제약 없음 -- 메인 스레드에서 작동합니다.
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // DefaultDispatcher에 디스패치됩니다.
    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


launch { ... }를 매개변수 없이 사용하면 실행 블록이 시작된 CoroutineScope의 컨텍스트(따라서 디스패처)를 상속합니다. 이 경우 주 실행 블록이 메인 스레드에서 실행되는 주 실행 블록의 컨텍스트를 상속합니다.

Dispatchers.Unconfined는 메인 스레드에서 실행되는 것처럼 보이지만 실제로는 나중에 설명할 다른 메커니즘입니다.

기본 디스패처는 범위에서 명시적으로 지정된 다른 디스패처가 없을 때 사용됩니다. Dispatchers.Default로 나타내며 스레드의 공유된 백그라운드 풀을 사용합니다.

newSingleThreadContext는 코루틴을 실행하기 위해 스레드를 만듭니다. 전용 스레드는 매우 비싼 리소스입니다. 실제 응용 프로그램에서는 더 이상 필요하지 않을 때 close 함수를 사용하여 해제하거나 최상위 변수에 저장하고 응용 프로그램 전체에서 재사용해야 합니다.


Unconfined 대 confined 디스패처


Dispatchers.Unconfined 코루틴 디스패처는 호출자 스레드에서 코루틴을 시작하지만 첫 번째 중단 지점까지만입니다. 중단 후 중단된 함수에 의해 완전히 결정되는 스레드에서 코루틴을 다시 시작합니다. 비 CPU 시간을 사용하지 않거나 특정 스레드로 제한되는 공유 데이터(예: UI)를 업데이트하지 않는 코루틴에 적합합니다.

반면, 디스패처는 기본적으로 외부 CoroutineScope에서 상속됩니다. 특히 runBlocking 코루틴의 기본 디스패처는 호출자 스레드에 제한되어 있으므로 상속하면 예측 가능한 FIFO 스케줄링을 사용하여이 스레드에 대한 실행이 제한됩니다.

launch(Dispatchers.Unconfined) { // 제약 없음 -- 메인 스레드에서 작동합니다.
    println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
    delay(500)
    println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
}
launch { // 부모 코루틴인 주 실행 블록의 컨텍스트
    println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
    delay(1000)
    println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}


다음 출력을 생성합니다:

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main


따라서 runBlocking {...}에서 상속한 컨텍스트로부터 시작된 코루틴은 메인 스레드에서 계속 실행되며, Unconfined 디스패처는 delay 함수가 사용하는 기본 실행자 스레드에서 다시 시작됩니다.

Unconfined 디스패처는 코루틴을 나중에 실행하기 위해 디스패치가 필요하지 않거나 원하지 않는 부작용을 생성하는 특정 코너 케이스에서 도움이 되는 고급 메커니즘입니다. 일반 코드에서 Unconfined 디스패처를 사용해서는 안 됩니다.

 

코루틴 및 스레드 디버깅


코루틴은 하나의 스레드에서 중단하고 다른 스레드에서 다시 시작할 수 있습니다. 심지어 단일 스레드 디스패처를 사용하더라도 특수한 도구 없이는 코루틴이 어떤 작업을 하고 어디에서 언제 수행되는지 이해하기 어려울 수 있습니다.


IDEA를 사용한 디버깅


Kotlin 플러그인의 Coroutine Debugger는 IntelliJ IDEA에서 코루틴 디버깅을 간단하게 만듭니다.

디버깅은 kotlinx-coroutines-core의 버전 1.3.8 이상에서 작동합니다.

디버그 도구 창에는 "Coroutines" 탭이 있습니다. 이 탭에서 현재 실행 중인 코루틴과 중단된 코루틴에 대한 정보를 찾을 수 있습니다. 코루틴은 실행 중인 디스패처에 따라 그룹화됩니다.

 


코루틴 디버깅을 사용하면 다음을 수행할 수 있습니다.

  • 각 코루틴의 상태를 확인합니다.
  • 실행 중인 코루틴과 중단된 코루틴의 로컬 및 캡처된 변수의 값을 확인합니다.
  • 전체 코루틴 생성 스택과 코루틴 내부의 호출 스택을 확인합니다. 스택에는 표준 디버깅 중에 손실되는 변수 값도 모두 포함됩니다.
  • 각 코루틴과 해당 스택의 상태를 포함하는 전체 보고서를 얻습니다. 이를 얻으려면 "Coroutines" 탭에서 마우스 오른쪽 버튼을 클릭한 다음 "Get Coroutines Dump"를 클릭하십시오.


코루틴 디버깅을 시작하려면 중단점을 설정하고 애플리케이션을 디버그 모드로 실행하기만 하면 됩니다.


로그를 사용한 디버깅


코루틴 디버거가 없는 스레드로 응용 프로그램을 디버깅하는 또 다른 접근 방식은 각 로그 문에서 로그 파일에 스레드 이름을 출력하는 것입니다. 이 기능은 모든 로깅 프레임워크에서 일반적으로 지원됩니다. 코루틴을 사용할 때 스레드 이름만으로는 맥락을 파악하기 어려울 수 있으므로 kotlinx.coroutines는 디버깅 기능을 제공하여 이를 더 쉽게 만듭니다.

다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션과 함께 실행하십시오.

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()}")


세 개의 코루틴이 있습니다. runBlocking 내부의 주 코루틴(#1)과 값 a(#2) 및 b(#3)을 계산하는 두 개의 코루틴입니다. 모두 runBlocking의 컨텍스트에서 실행되며 메인 스레드에 제한됩니다. 이 코드의 출력은 다음과 같습니다:

[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


log 함수는 스레드의 이름을 대괄호로 묶어 출력하며, 현재 실행 중인 코루틴의 식별자가 추가됩니다. 이 식별자는 디버깅 모드가 켜져 있는 경우 모든 생성된 코루틴에 연속적으로 할당됩니다.

JVM 옵션 -Dkotlinx.coroutines.debug로 이 코드를 실행할 때 디버깅 모드도 켜집니다. 디버깅 기능에 대한 자세한 내용은 DEBUG_PROPERTY_NAME 속성의 문서를 참조하십시오.


스레드 간 이동


다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션과 함께 실행하십시오.

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")
            }
        }
    }
}


이 코드는 몇 가지 새로운 기술을 보여줍니다. 하나는 명시적으로 지정된 컨텍스트와 함께 runBlocking을 사용하는 것이며, 다른 하나는 여전히 동일한 코루틴 내에서 컨텍스트를 변경하기 위해 withContext 함수를 사용하는 것입니다. 출력에서 다음을 확인할 수 있습니다.

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1


이 예제는 더 이상 필요하지 않은 경우 Kotlin 표준 라이브러리의 use 함수를 사용하여 newSingleThreadContext로 생성된 스레드를 해제하기도 합니다.


컨텍스트 내의 Job


코루틴의 Job은 해당 컨텍스트의 일부이며 coroutineContext[Job] 식을 사용하여 가져올 수 있습니다.

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


디버그 모드에서는 다음과 같이 출력됩니다.

 

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

 

CoroutineScope의 isActive는 coroutineContext[Job]?.isActive == true의 편리한 바로 가기입니다.

 

코루틴의 자식들


코루틴이 다른 코루틴의 CoroutineScope에서 시작되면 CoroutineScope.coroutineContext를 통해 그 컨텍스트를 상속하며, 새 코루틴의 Job은 부모 코루틴의 Job의 자식이 됩니다. 부모 코루틴이 취소되면 그 모든 자식들도 재귀적으로 취소됩니다.

그러나 이 부모-자식 관계는 다음 두 가지 방법 중 하나로 명시적으로 무시할 수 있습니다.

1. 다른 범위를 명시적으로 지정하여 코루틴을 시작할 때(예: GlobalScope.launch)는 부모 범위에서 Job을 상속하지 않습니다.

2. 새 코루틴의 콘텍스트로 다른 Job 객체를 전달하면(아래 예제 참조), 부모 범위의 Job을 무시합니다.

두 경우 모두 시작된 코루틴은 시작된 범위와 관련이 없으며 독립적으로 작동합니다.

다음은 이를 보여주는 예제 코드입니다.

// 어떤 종류의 들어오는 요청을 처리하기 위해 코루틴을 시작합니다.
val request = launch {
    // 두 개의 다른 작업을 시작합니다.
    launch(Job()) { 
        println("job1: 내 자신의 Job에서 독립적으로 실행됩니다!")
        delay(1000)
        println("job1: 요청의 취소에 영향받지 않습니다")
    }
    // 다른 작업은 부모 컨텍스트를 상속합니다.
    launch {
        delay(100)
        println("job2: 요청 코루틴의 자식입니다")
        delay(1000)
        println("job2: 부모 요청이 취소되면 이 라인을 실행하지 않을 것입니다")
    }
}
delay(500)
request.cancel() // 요청 처리를 취소합니다.
println("main: 요청 취소 후 살아남은 것은 무엇인가요?")
delay(1000) // 주 스레드를 1초 동안 지연시켜 결과 확인


이 코드의 출력은 다음과 같습니다.

job1: 내 자신의 Job에서 독립적으로 실행됩니다!
job2: 요청 코루틴의 자식입니다
main: 요청 취소 후 살아남은 것은 무엇인가요?
job1: 요청의 취소에 영향받지 않습니다


부모-자식 관계


부모 코루틴은 항상 모든 자식의 완료를 기다립니다. 부모는 자식을 명시적으로 추적할 필요가 없으며, 자식들을 기다리기 위해 Job.join을 사용할 필요도 없습니다.

다음은 이를 보여주는 예제 코드입니다.

// 어떤 종류의 들어오는 요청을 처리하기 위해 코루틴을 시작합니다.
val request = launch {
    repeat(3) { i -> // 몇 개의 자식 작업을 시작합니다.
        launch  {
            delay((i + 1) * 200L) // 변수 지연 200ms, 400ms, 600ms
            println("Coroutine $i is done")
        }
    }
    println("request: 나는 완료되었고, 활성 상태인 자식을 명시적으로 기다릴 필요가 없습니다.")
}
request.join() // 요청 및 그 자식 모두의 완료를 기다립니다.
println("이제 요청 처리가 완료되었습니다.")


결과는 다음과 같을 것입니다.

request: 나는 완료되었고, 활성 상태인 자식을 명시적으로 기다릴 필요가 없습니다.
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
이제 요청 처리가 완료되었습니다.

 

코루틴 디버깅을 위해 코루틴에 이름 지정하기


자동으로 할당된 ID는 코루틴이 자주 로깅하고 로그 레코드를 동일한 코루틴에서 나오는 것과 연결해야 할 때 유용합니다. 그러나 코루틴이 특정 요청을 처리하거나 특정 백그라운드 작업을 수행하는 경우, 디버깅 목적으로 명시적으로 이름을 지정하는 것이 좋습니다. CoroutineName 콘텍스트 요소는 스레드 이름과 동일한 목적으로 사용됩니다. 디버깅 모드가 켜져 있을 때 이 코루틴을 실행하는 스레드 이름에 포함됩니다.

다음 예제는 이 개념을 보여줍니다.

log("Started main coroutine")
// 두 개의 백그라운드 값 계산 실행
val v1 = async(CoroutineName("v1coroutine")) {
    delay(500)
    log("Computing v1")
    252
}
val v2 = async(CoroutineName("v2coroutine")) {
    delay(1000)
    log("Computing v2")
    6
}
log("The answer for v1 / v2 = ${v1.await() / v2.await()}")


-Dkotlinx.coroutines.debug JVM 옵션과 함께 실행할 때 생성되는 출력은 다음과 유사합니다.

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42


콘텍스트 요소 결합


때로는 코루틴 콘텍스트에 여러 요소를 정의해야 할 필요가 있습니다. 이를 위해 + 연산자를 사용할 수 있습니다. 예를 들어, 명시적으로 지정된 디스패처와 함께 명시적으로 이름을 지정하여 코루틴을 시작할 수 있습니다.

launch(Dispatchers.Default + CoroutineName("test")) {
    println("I'm working in thread ${Thread.currentThread().name}")
}


-Dkotlinx.coroutines.debug JVM 옵션과 함께 실행할 때 이 코드의 출력은 다음과 같습니다.

I'm working in thread DefaultDispatcher-worker-1 @test#2

 

코루틴 스코프


컨텍스트, 자식 및 작업에 대한 지식을 합쳐보겠습니다. 애플리케이션에 수명주기가 있는 객체가 있지만 그 객체는 코루틴이 아닌 경우를 가정해 봅시다. 예를 들어, Android 애플리케이션을 작성하고 Android 활동의 콘텍스트에서 다양한 코루틴을 시작하여 데이터를 비동기적으로 가져오고 업데이트하거나 애니메이션을 수행해야 할 수 있습니다. 이러한 모든 코루틴은 액티비티가 파괴될 때 메모리 누수를 방지하기 위해 취소되어야 합니다. 물론 컨텍스트와 작업을 수동으로 조작하여 액티비티와 그 코루틴의 수명을 연결할 수 있지만, kotlinx.coroutines는 그것을 캡슐화하는 추상화를 제공합니다: CoroutineScope. 모든 코루틴 빌더는 이미 이를 확장으로 선언했으므로 코루틴 스코프에 이미 익숙할 것입니다.

우리는 액티비티의 수명주기에 연결된 CoroutineScope 인스턴스를 생성함으로써 코루틴의 수명주기를 관리합니다. CoroutineScope 인스턴스는 CoroutineScope() 또는 MainScope() 팩토리 함수로 생성할 수 있습니다. 전자는 일반 용도의 스코프를 생성하고, 후자는 UI 애플리케이션을 위한 스코프를 생성하며 기본 디스패처로 Dispatchers.Main을 사용합니다.

class Activity {
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }

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


이제 우리는 이 Activity에 연결된 스코프를 사용하여 이 Activity의 범위에서 코루틴을 시작할 수 있습니다. 데모를 위해 doSomething에서 시작된 10개의 코루틴이 서로 다른 시간 동안 지연됩니다.

메인 함수에서 우리는 액티비티를 생성하고 테스트 doSomething 함수를 호출하며, 500ms 후에 액티비티를 파괴합니다. 이로써 doSomething에서 시작된 모든 코루틴이 취소됩니다. 액티비티가 파괴된 후에는 더 이상 메시지가 출력되지 않으며 조금 더 기다려도 마찬가지입니다.

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.destroy()에서 한 번의 호출로 모두 취소됩니다.

참고로 Android는 수명주기가 있는 모든 엔터티에서 코루틴 스코프에 대한 공식 지원을 제공합니다. 관련 문서를 참조하세요.


스레드 로컬 데이터


때로는 코루틴에 스레드 로컬 데이터를 전달하거나 코루틴 간에 스레드 로컬 데이터를 전달하는 기능이 편리할 수 있습니다. 그러나 코루틴은 특정 스레드에 바인딩되어 있지 않으므로 이 작업을 수동으로 수행하면 보일러플레이트 코드가 발생할 가능성이 높습니다.

ThreadLocal의 경우, asContextElement 확장 함수가 도움을 줍니다. 이 함수는 주어진 ThreadLocal의 값을 유지하고 코루틴이 컨텍스트를 전환할 때마다 해당 값을 복원하는 추가적인 컨텍스트 요소를 생성합니다.

이를 쉽게 실제로 시연할 수 있습니다.

threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
    println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    yield()
    println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")


이 예제에서는 Dispatchers.Default를 사용하여 백그라운드 스레드 풀에서 새 코루틴을 시작합니다. 따라서 코루틴이 다른 스레드에서 실행되더라도 threadLocal 변수를 지정하여 지정한 스레드 로컬 변수 값을 유지합니다. 결과적으로 출력은 다음과 같습니다(디버그 모드로 실행).

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'


코루틴이 실행되는 스레드가 다르더라도 threadLocal 변수의 값을 유지합니다. 이것이 asContextElement를 사용하는 방법입니다.

ThreadLocal은 kotlinx.coroutines에서 제공하는 모든 기본 기능과 함께 사용할 수 있습니다. 그러나 ThreadLocal이 한 가지 중요한 제한 사항이 있습니다. 스레드 로컬이 변경되면 새 값을 코루틴 호출자로 전파하지 않으며(컨텍스트 요소는 모든 ThreadLocal 객체 액세스를 추적할 수 없으므로), 업데이트된 값을 다음 일시 중지에서 잃게 됩니다. 이러한 상황을 피하기 위해 코루틴에서 컨텍스트 요소를 업데이트하기 위해 withContext를 사용하고 자세한 내용은 asContextElement 문서를 참조하세요.

대안으로 가변 상자에 값을 저장하고 해당 가변 상자를 ThreadLocal 변수에 저장하는 방법도 있습니다. 그러나 이 경우에는 가변 상자의 변수를 가능한 동시에 수정하는 경우 동기화를 완전히 담당해야 합니다.

고급 사용을 위해 로깅 MDC, 트랜잭션 컨텍스트 또는 데이터 전달을 위해 내부적으로 스레드 로컬을 사용하는 기타 라이브러리와 통합하기 위한 스레드 컨텍스트 요소 인터페이스에 대한 문서를 참조하세요.

 

원문

 

https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

반응형

댓글