오늘도 더 나은 코드를 작성하였습니까?

Coroutine context and dispatchers 본문

Coroutine/coroutineBasic

Coroutine context and dispatchers

hik14 2022. 7. 6. 17:36

코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext type로 표시되는 context에서 실행됩니다.

 

coroutne context

-  다양한 요소의 집합

- 주요 요소는 이전에 본 코루틴의 job / dispatcher

Dispatchers and threads

코루틴 context에는 해당 코루틴이 실행에 사용하는 스레드를 결정하는 코루틴 디스패처(CoroutineDispatcher 참조)가 포함됩니다

코루틴 디스패처는 코루틴 실행을 특정 스레드로 제한하거나, 스레드 풀에 디스패치하거나, 제한 없이(unconfined)  실행되도록 할 수 있습니다.

 

launch{ ... } 및 async {...} 와 같은 모든 코루틴 빌더는 새 코루틴 및 기타 컨텍스트 요소에 대한 디스패처를 명시적으로 지정하는 데 사용할 수 있는 선택적 CoroutineContext 매개변수 넣어줄 수 있다.

launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
}

launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
}

launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default               : I'm working in thread ${Thread.currentThread().name}")
}

launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}

Unconfined : I'm working in thread main @coroutine#3

Default : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4

newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#5

main runBlocking : I'm working in thread main @coroutine#2

 

 

launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}

launch { ... }가 매개변수 없이 사용되면 시작되는 CoroutineScope에서 컨텍스트(디스패처)를 상속합니다.

이 경우 main Thread에서 실행되는 메인 runBlocking 코루틴의 컨텍스트를 상속하여  main Thread에서 실행된다.

launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
}

Dispatchers.Unconfined는 메인 스레드에서도 실행되는 것처럼 보이지만 실제로는 좀 더 복잡하고 다릅니다.

launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
    println("Default : I'm working in thread ${Thread.currentThread().name}")
}

 

특정 디스패처가 scope에 명시적으로 지정되지 않은 경우 default dispatcher가 사용됩니다.

Dispatchers.Default로 표시되며 스레드의 sharded background pool을 사용합니다.

launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
    println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}

newSingleThreadContext는 코루틴이 실행할 스레드를 직접생성합니다.

전용 스레드를 생성하여 실행시키는것 은 매우 비싼 리소스입니다.

실제 app에서는 더 이상 필요하지 않을 때 닫기 기능을 사용하여 해제하거나 최상위 변수에 저장하고 app 전체에서 재사용해야 합니다.

 

Unconfined vs confined dispatcher

Dispatchers.Unconfined 코루틴 디스패처는 caller Thread에서 코루틴을 시작하지만 첫 번째 suspend 지점까지만 실행합니다.

일시 중단 후 호출된 일시 suspend function 에 의해  결정된 스레드에서 코루틴을 다시 시작합니다.

Unconfined 디스패처는 CPU 시간을 소비하지 않고,  특정 스레드에 국한된 공유 데이터(예: UI)를 업데이트하지 않는 코루틴에 적합.

반면에 디스패처는 기본적으로 외부 CoroutineScope에서 상속됩니다. 특히 runBlocking 코루틴의 기본 디스패처는 caller Thread로 제한되므로 이를 상속하면 예측 가능한 FIFO 스케줄링으로 caller Thread로 실행을 제한하는 효과가 있습니다.

launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
    delay(500)
    println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}

launch { // context of the parent, main runBlocking coroutine
    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 @coroutine#2

main runBlocking: I'm working in thread main @coroutine#3

Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor @coroutine#2

main runBlocking: After delay in thread main @coroutine#3

 

runBlocking { ... }에서 상속된 컨텍스트가 있는 코루틴은 메인 스레드에서 계속 실행되는 반면,

제한되지 않은 코루틴은 suspend함수(delay)가 사용하는 default Thread에서 재개됩니다.

 

Unconfined Dispatcher는 코루틴의 일부 작업이 즉시 수행되어야 하기 때문에 나중에 실행을 위해 코루틴을 디스패치할 필요가 없거나, 일부러 바람직하지 않은 부작용을 생성하는 특정 경우에 도움이 될 수 있는 고급 메커니즘입니다.

Unconfined Dispatcher는 일반 코드에서 사용하면 안 됩니다.

Jumping between threads

    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                println("Started in ${Thread.currentThread().name}")
                withContext(ctx2) {
                    println("Working in ${Thread.currentThread().name}")
                }
                println("Back to ${Thread.currentThread().name}")
            }
        }
    }

 

새로운 기술을 살펴보자.

하나는 명시적으로 지정된 컨텍스트와 함께 runBlocking을 사용하는 것

다른 하나는 동일한 코루틴에 계속 유지하면서 withContext 함수를 사용하여 코루틴의 컨텍스트를 변경

*더 이상 필요하지 않을 때 newSingleThreadContext로 생성된 스레드를 해제하기 위해 Kotlin 표준 라이브러리의 use 함수도 사용합니다.

Job in the context

코루틴의 Job은 context 일부이며 coroutineContext[Job] 표현식을 사용하여 검색할 수 있다

fun main(): Unit = runBlocking {

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

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

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

    launch {

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

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

CoroutineScope의 isActive는 coroutineContext[Job]?.isActive == true의 편리한 단축키이다.

Children of a coroutine

코루틴이 다른 코루틴의 CoroutineScope에서 시작되면 CoroutineScope.coroutineContext를 통해 컨텍스트를 상속하고, 새 코루틴의 job은 상위 코루틴 job의 자식이 됩니다.

부모 코루틴이 취소되면 모든 자식도 재귀적으로 취소됩니다.

 

하지만, 부모-자식 관계는 다음 두 가지 방법 중 하나로 명시적으로 재정의될 수 있습니다

 

1.  코루틴을 시작할 때 새로운 CoroutineScope가 명시적으로 지정되면(예: GlobalScope.launch) 상위 범위에서 작업을 상속하지 않습니다.

 

2. Job 객체가 새 코루틴에 대한 컨텍스트로 전달되면(아래 예와 같이) 상위 범위의 Job을 재정의합니다.

 

// launch a coroutine to process some kind of incoming request
val request = launch {
    // it spawns two other jobs
    launch(Job()) { 
        println("job1: I run in my own Job and execute independently!")
        delay(1000)
        println("job1: I am not affected by cancellation of the request")
    }
    // and the other inherits the parent context
    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() // cancel processing of the request
println("main: Who has survived request cancellation?")
delay(1000) // delay the main thread for a second to see what happens

Parental responsibilities

부모 코루틴은 항상 모든 자식 코루틴의 작업이 완료될 때까지 기다립니다.

부모는 시작하는 모든 자식을 명시적으로 추적할 필요가 없으며,  마지막에 자식을 기다리기 위해 Job.join을 사용할 필요도 없다. 

// launch a coroutine to process some kind of incoming request
val request = launch {
    repeat(3) { i -> // launch a few children jobs
        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() // wait for completion of the request, including all its children
println("Now processing of the request is complete")

Naming coroutines for debugging

자동으로 할당된 ID는 코루틴이 자주 log를 남길때  유용하며 동일한 코루틴에서 오는 로그 기록의 상관 관계를 지정때 유용하다.

그러나 코루틴이 특정 요청을 처리하거나 특정 백그라운드 작업을 수행하는 것과 관련된 경우 디버깅 목적으로 명시적으로 이름을 지정하는 것이 좋습니다.

 CoroutineName 컨텍스트 요소는 스레드 이름과 동일한 용도로 사용됩니다. 디버깅 모드가 켜져 있을 때 이 코루틴을 실행하는 스레드 이름에 포함됩니다.

log("Started main coroutine")
// run two background value computations
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()}")

Combining context elements

때로는 코루틴 컨텍스트에 대해 여러 요소를 정의해야 합니다. 이를 위해 + 연산자를 사용할 수 있습니다.

예를 들어 명시적으로 지정된 디스패처와 명시적으로 지정된 이름으로 동시에 코루틴을 시작할 수 있습니다.

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

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