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

Coroutine scope 본문

Coroutine/coroutineBasic

Coroutine scope

hik14 2022. 7. 7. 15:27

context에 대한 지식을 children 및 jobs과 함께 가지고 있어야 한다.

 

애플리케이션에 수명 주기가 있는 객체가 있지만 해당 객체는 코루틴이 아니라고 가정해보자

예를 들어, 우리는 Android 애플리케이션을 작성하고 데이터를 가져오고 업데이트하고, 애니메이션을 수행하는 등의 비동기 작업을 수행하기 위해 Android Activity의 context에서 다양한 코루틴을 시작합니다.

 

모든 코루틴은 memory leak를 방지하기 위해 Activity이 파괴될 때 취소되어야 한다. 물론, context와 job을 수동으로 설정하여 Acitvity의 생명주기에 해당 코루틴을 연결할 수 있지만 kotlinx.coroutines는 다음을 캡슐화하는 추상화를 제공합니다.

coroutine Builder가 coroutineScope의 extention 으로 선언되므로 coroutineScope에 이미 익숙해야 합니다.

 

class Activity {
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }
    // to be continued ...

이제 정의된 coroutineScope를 사용하여 이 Activity의 ​​범위에서 코루틴을 시작할 수 있습니다. 데모를 위해 다른 시간 동안 지연되는 10개의 코루틴을 시작합니다.

// class Activity continues
    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

우리의 main function에서, 우리는 activity를 생성하고, Test용 doSomething function를 호출하고, 500ms 후에 activity를 파괴합니다.

val activity = Activity()
activity.doSomething() // run test function
println("Launched coroutines")
delay(500L) // delay for half a second
println("Destroying activity!")
activity.destroy() // cancels all coroutines
delay(1000) // visually confirm that they don't work

Launched coroutines

Coroutine 0 is done

Coroutine 1 is done

Destroying activity!

 

처음 두 코루틴만 메시지를 인쇄하고 나머지 코루틴은 Activity.destroy()에서 job.cancel()을 한 번 호출하여 취소됩니다.

destroying activity 이후 더 이상 출력이 없다.

 

Android는 수명 주기가 있는 모든 엔터티에서 코루틴 범위에 대한 자사 지원을 제공합니다. 

Thread-local data

때때로 스레드 로컬 데이터를 코루틴으로 또는 코루틴 간에 전달할 수 있는 기능이 있으면 편리합니다.

1. Thread:  --data--> coroutine

2. Thread: coroutine <--data--> coroutine

 

그러나, 코루틴은 특정 스레드에 바인딩되지 않기 때문에 수동으로 수행하면 상용구로 이어질 수 있습니다.

ThreadLocal의 asContextElement extention function을 통해 할수 있습니다.

지정된 ThreadLocal의 값을 유지하고, 코루틴이 컨텍스트를 전환할 때마다 이를 복원하는 추가 컨텍스트 요소를 생성합니다.

fun main(): Unit = runBlocking {

    val threadLocal = ThreadLocal<String>()
    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를 사용하여 background Thread pool 에서 새 코루틴을 launch{...} 로 빌더를 이용하여 시작시킨다.

다른 Thread pool 과 Thread에서 작동하지만, 여전히 threadLocal.asContextElement(value)를 사용하여 지정한 threadlocal 변수 값을 확인할 수 있다. 코루틴이 실행되는 스레드에 관계없이.

 

해당 context eletment를 설정하는 것을 잊기 쉽습니다.

코루틴에서 접근하는 Thread local 변수는 코루틴을 실행하는 스레드가 다른 경우 예기치 않은 값을 가질 수 있습니다.

이러한 상황을 피하려면 잘못된 사용에 대해 surePresent 메서드와 fail-fast를 사용하는 것이 좋습니다.

 

ThreadLocal은 최고 수준의 지원을 제공하며 모든 기본 kotlinx.coroutines가 제공하는 것과 함께 사용할 수 있습니다. 

그러나 한 가지 주요 제한 사항이 있습니다.

fun main(): Unit = runBlocking {

    val myThreadLocal = ThreadLocal<String>()
    myThreadLocal.set("main")
    
    println("current Thread: ${Thread.currentThread().name} myThreadLocal: ${myThreadLocal.get()}")

    withContext(Dispatchers.IO) {
        println("current Thread: ${Thread.currentThread().name} myThreadLocal: ${myThreadLocal.get()}") // Prints "main"
        myThreadLocal.set("UI")
    }

    println("current Thread: ${Thread.currentThread().name} myThreadLocal: ${myThreadLocal.get()}")
}

ThreadLocal 이 변경되면 새 값이 코루틴 호출자에게 전파되지 않고(context 요소가 모든 ThreadLocal 개체 액세스를 추적할 수 없기 때문에) 업데이트된 값이 다음 일시 중단 시 손실됨.

 

fun main(): Unit = runBlocking {

    //new Coroutine1
    launch {
        println("currentThread: ${Thread.currentThread().name}, Launch1 before set : ThreadLocal : ${tl.get()}")
        tl.set("Coroutine1 modified tl")
        println("currentThread: ${Thread.currentThread().name}, Launch1 after set : ThreadLocal : ${tl.get()} ")

    }.join()

    //new Coroutine2
    launch {
        println("currentThread: ${Thread.currentThread().name}, Launch2 before set : ThreadLocal : ${tl.get()}")
        tl.set("Coroutine2 modified tl")
        println("currentThread: ${Thread.currentThread().name}, Launch2 after set : ThreadLocal : ${tl.get()} ")
    }

    delay(1000)
    println("main : ThreadLocal : ${tl.get()} ")
}

 

 

 

withContext를 사용하여 코루틴에서 ThreadLocal 값을 업데이트합니다. 자세한 내용은 asContextElement를 참조,

 

asContextElement

 - ThreadLocal을 ThreadContextElement로 래핑합니다.

- ThreadContextElement는 재개되는 실제 스레드와 상관없이 코루틴에 대해 주어진 ThreadLocal 값을 유지

val tl = ThreadLocal.withInitial { "initial" }

fun main(): Unit = runBlocking(tl.asContextElement("main")) {
    
    println("currentThread: ${Thread.currentThread().name} tlValue: ${tl.get()}")
    
    withContext(Dispatchers.IO+tl.asContextElement("context1")) {
        println("currentThread: ${Thread.currentThread().name} tlValue: ${tl.get()}")
        // Change context
        withContext(tl.asContextElement("context2")) {
            println("currentThread: ${Thread.currentThread().name} tlValue: ${tl.get()}")
        }
        // Context is changed again
        println("currentThread: ${Thread.currentThread().name} tlValue: ${tl.get()}")
    }
    
    println("currentThread: ${Thread.currentThread().name} tlValue: ${tl.get()}")

 

Counter(var i: Int) 클래스와 같은 변경 가능한 상자에 값을 저장할 수 있으며, 이는 차례로 스레드 로컬 변수에 저장됩니다. 그러나 이 경우 이 변경 가능한 상자의 변수에 대한 잠재적인 동시 수정 사항을 동기화하는 것은 전적으로 귀하의 책임입니다.

 

고급 사용(예: 로깅 MDC, 트랜잭션 컨텍스트 또는 데이터 전달을 위해 내부적으로 스레드 로컬을 사용하는 기타 라이브러리와의 통합)의 경우 구현해야 하는 ThreadContextElement 인터페이스의 설명서를 참조