Coroutine Scope
Context, 자식 그리고 Job들에 대한 지식을 결합시켜보자. 어플리케이션이 Coroutine이 아닌 생명주기을 가진 객체를 가지고 있다고 가정해보자. 예를 들어, 안드로이드 어플리케이션을 만들고 안드로이드 Activity의 Context 상에서 데이터를 가져오고 업데이트 시키거나, 애니메이션을 실행하는 등의 비동기 작업들을 수행하기 위해서 다양한 Coroutine들을 실행시킬 수 있다. 이 Coroutine들은 Activity가 파괴될 때 메모리 누수를 방지하기 위해 취소되어야 한다. 물론 Context와 Job들을 직접 조작하여 Activity의 Coroutine의 생명주기를 결합시킬 수 있다. 하지만, kotlinx.coroutines 패키지는 CoroutineScope을 캡슐화하는 추상화를 제공한다.
우리는 Activity의 생명주기에 묶은 CoroutineScope의 인스턴스를 생성해 Coroutines의 생명주기를 관리한다. CoroutineScope 인스턴스는 CoroutineScope() 이나 MainScope() 같은 팩토리 함수들로 생성될 수 있다. 전자는 일반적인 목적의 Scope을 생성하며, 후자는 UI 어플리케이션을 위한 Scope을 생성하고 Dispatchers.Main을 기본 디스패쳐로 사용한다.
class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
// to be continued ...
이제 정의된 Scope을 사용해 Activity의 Scope 내에서 Coroutines를 실행시킬 수 있다. 데모를 위해 다른 시간의 delay를 가지는 10개의 Coroutine들을 생성하자.
// 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 함수에서 Activity를 생성하며, 테스트 함수인 doSomething 를 호출하고, Activity를 500ms 후에 파괴한다. 이는 doSomething에서 실행된 모든 Coroutine들을 취소한다. 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!
첫 두개의 Coroutine들만 메세지를 출력하고 나머지들은 Activity.destory()에서 job.cancel()이 한 번 호출되어 취소되는 것을 확인할 수 있다.
📖 안드로이드는 수명주기가 있는 모든 엔티티들*1에서 CoroutineScope에 대한 자사의 지원을 제공한다. 다음 문서를 참조하자.
Thread-local 데이터
Thread-local 데이터를 Coroutine으로 전달하거나, Coroutine들간에 전달하는 기능이 있으면 편리하다. 하지만, Coroutine들은 특정 스레드에 묶여있지 않기 때문에 이를 직접하게 되면 보일러 플레이트를 만들 수 있다.
이를 해결하기 위해 ThreadLocal을 위한 asContextElement 확장 함수가 있다. 이는 Coroutine이 Context를 변경할 때마다 주어진 ThreadLocal의 값을 유지하고 복원하는 추가적인 Context 구성요소를 생성한다.
이는 직접 보면 쉽게 설명된다 :
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()}'")
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 예에서 우리는 Dispatcher.Default를 사용하여 백그라운드 스레드풀에서 새로운 Coroutine을 실행한다. 따라서 스레드풀과 다른 스레드에서 동작하지만, 어떤 스레드에서 동작하던지 상관 없이 threadLocal.asContextElement(value = "launch")를 사용해 지정한 Thread-local 변수 값을 가지고 있다. 따라서 결과 값은 다음과 같다.
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'
해당 Context 요소를 설정하는 것을 잊기 쉽다. 만약 Coroutine을 실행하는 스레드가 다르다면 스레드에 의해 액세스된 Thread-local 변수는 예측할 수 없는 값을 가질 수 있다. 이러한 상황을 방지하기 위해 ensurePresent 메서드를 사용하고 잘못된 사용이 있을 시 fail-fast*2 하는 것이 권장된다.
ThreadLocal은 최고의 지원을 제공하며, kotlinx.coroutines 패키지의 모든 원시 요소들과 함께 사용할 수 있다. 그러나 이는 하나의 주요한 제한사항을 가진다 : 만약 Thread-local이 변경되면, 새로운 값은 코루틴을 호출한 곳에 전달되지 않고(Context 요소가 모든 ThreadLocal 객체로의 접근을 추적할 수 없기 때문에), 변경된 값은 다음 일시 중단시점에 손실된다. Coroutine 내의 Thread-local을 변경하기 위해서는 withContext를 사용하자. 더 자세한 것은 asContextElement를 참조.
또는 Thread-local 변수에 class Counter(var i: Int) 와 같은 변경 가능한 박스*3를 저장할 수 있다. 하지만, 이 경우 변경 가능한 박스의 변수값이 동시 접근되어 바뀌는 것에 대해 동기화할 모든 책임이 생긴다.
로깅 MDC와의 통합, transactional contexts, 혹은 데이터 전달을 위해 내부적으로 Thread-local을 사용하는 다른 라이브러리들과 같은 Thread-local 고급 사용법은 구현되어야 하는 interface를 설명 해놓은 ThreadContextElement 문서를 참고하면 된다.
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. Activity와 ViewModel 등을 뜻한다. Activity에서는 lifecycleScope를 제공하고 ViewModel은 viewModelScope을 제공한다.
*2. 빠르게 실패하도록 하는 것을 뜻한다.
*3. 변경 가능한 박스란, 변수가 클래스로 감싸진 클래스를 뜻한다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Coroutine context and dispatchers - Coroutine scope
원문 최종 수정 : 2022년 6월 27일
목차로 돌아가기