•
코루틴은 항상 CoroutineContext 타입의 값으로 표현되는 특정 컨텍스트에서 실행된다.
•
이 컨텍스트는 다양한 요소들로 구성되어 있으며 주요 요소로는 코루틴의 Job과 CoroutineDispatcher가 있다.
디스패처와 스레드
•
코루틴 컨텍스트에는 코루틴이 실행될 스레드를 결정하는 CoroutineDispatcher가 포함된다.
•
디스패처는 코루틴 실행을 특정 스레드에 제한할 수 있고 스레드 풀로 전달하거나 구속받지 않고 실행되도록 할 수 있다.
•
모든 코루틴 빌더(launch, async 등)는 CoroutineContext 파라미터를 선택적으로 받아서 새로운 코루틴에 디스패처나 다른 컨텍스트 요소를 명시적으로 지정할 수 있다.
launch { // 부모 컨텍스트, main runBlocking 코루틴의 컨텍스트
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // 구속되지 않음 -- main 스레드에서 실행
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
Kotlin
복사
•
파라미터 없는 launch { … }
◦
실행 중인 코루틴 스코프에서 컨텍스트(디스패처 포함)를 상속받는다.
◦
이 경우 main runBlocking 코루틴의 컨텍스트를 상속받아 메인 스레드에서 실행된다.
•
Dispatchers.Unconfined
◦
메인 스레드에서 실행되는 것처럼 보이지만 사실은 다른 메커니즘이다.
•
Dispatchers.Default
◦
명시적인 디스패처가 지정되지 않은 경우 사용되는 기본 디스패처이다.
◦
백그라운드의 공유 스레드 풀을 사용한다.
•
newSingleThreadContext
◦
코루틴이 실행될 새로운 스레드를 생성한다.
◦
전용 스레드는 매우 비싼 자원이므로 실제 어플리케이션은 더 이상 필요하지 않을 때 close 함수로 해제하거나 최상위 변수로 저장하여 어플리케이션 전반에 걸쳐 재사용해야 한다.
제한되지 않은 디스패처 vs 제한된 디스패처
•
Dispatchers.Unconfined 코루틴 디스패처는 호출자 스레드에서 코루틴을 시작하지만 첫 번째 중단 지점까지만 실행된다.
•
중단 후에는 중단된 함수를 호출한 스레드에 따라 코루틴이 재개된다.
•
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 { // 부모 컨텍스트, main runBlocking 코루틴의 컨텍스트
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
Kotlin
복사
•
runBlocking { … } 의 컨텍스트를 상속받은 코루틴은 계속해서 메인 스레드에서 실행되지만 Unconfined 디스패처를 사용한 코루틴은 delay 함수가 사용되는 기본 실행자 스레드에서 재개된다.
•
Unconfined 디스패처는 코루틴의 실행을 나중에 재개할 필요가 없거나 재개하는 것이 바람직하지 않은 부작용을 초래하는 특정 상황에서 유용한 고급 메커니즘이다.
•
그러나 일반적인 코드에서는 Unconfined 디스패처를 사용하는 것은 권장되지 않는다.
코루틴과 스레드 디버깅
•
코루틴은 한 스레드에서 중단되고 다른 스레드에서 재개될 수 있다.
•
단일 스레드 디스패처를 사용하더라도 특별한 도구가 없으면 코루틴이 무엇을 하고 있었는지, 어디에서, 언제 실행되고 있었는지 파악하기 어려울 수 있다.
IDEA로 디버깅하기
Kotlin 플러그인의 코루틴 디버거는 IntelliJ IDEA에서 코루틴 디버깅을 간소화한다.
디버깅은 kotlinx-coroutines-core의 버전 1.3.8 이상에서 작동한다.
•
코루틴 디버깅
◦
각 코루틴의 상태를 확인할 수 있다.
◦
실행 중인 코루틴과 중단된 코루틴의 로컬 변수와 캡쳐된 변수의 값을 볼 수 있다.
◦
코루틴 생성 스택과 코루틴 내부의 호출 스택을 볼 수 있다.
◦
이 스택은 변수 값이 포함된 모든 프레임을 포함하며 일반 디버깅 시 잃어버릴 수 있는 값도 포함된다.
◦
각 코루틴의 상태와 스택을 포함하는 전체 보고서를 얻을 수 있다.
◦
이를 얻으려면 코루틴 탭 내에서 마우스 우클릭을 하고 Get Coroutines Dump를 클릭하라.
로깅을 통한 디버깅
•
코루틴 디버거 없이 스레드가 포함된 어플리케이션을 디버깅하는 또 다른 방법은 각 로그 문에 대해 로그 파일에 스레드 이름을 출력하는 것이다.
•
이 기능은 모든 로깅 프레임워크에서 보편적으로 지원된다.
•
코루틴을 사용할 때는 스레드 이름만으로는 많은 맥락을 제공하지 않으므로 kotlinx.coroutines에서는 이를 쉽게 해주는 디버깅 기능을 포함하고 있다.
•
디버깅 기능을 사용하려면 JVM 옵션으로 -Dkotlinx.coroutines.debug 를 사용하라.
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
Kotlin
복사
•
log 함수는 대괄호 안에 스레드 이름을 출력하며 현재 실행 중인 코루틴의 식별자가 추가된 메인 스레드를 볼 수 있다.
•
이 식별자는 디버깅 모드가 활성화되면 생성된 모든 코루틴에 연속적으로 할당된다.
•
디버깅 모드는 JVM을 -ea 옵션으로 실행할 때도 활성화된다.
스레드 간 점프
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
Kotlin
복사
•
newSingleThreadContext를 통해 명시적으로 지정된 컨텍스트와 함께 runBlocking을 사용하여 코루틴 내에서 코루틴의 컨텍스트를 변경한다.
•
또한, withContext 함수를 사용하여 동일한 코루틴 내에서 코루틴의 컨텍스트를 변경한다.
•
Kotlin 표준 라이브러리의 use 함수를 사용하면 newSingleThreadContext로 생성된 스레드를 더 이상 필요하지 않을 때 해제할 수 있다.
컨텍스트의 Job
println("My job is ${coroutineContext[Job]}")
// 출력:
// My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
Kotlin
복사
•
코루틴의 Job은 코루틴 컨텍스트의 일부이며 coroutineContext[Job] 표현식을 사용하여 이를 가져올 수 있다.
•
CoroutineScope에서 isActive는 coroutineContext[Job]?.isActive == true의 편리한 축약이다.
코루틴의 자식
•
다른 코루틴의 CoroutineScope에서 코루틴이 시작되면 CoroutineScope.coroutineContext를 통해 부모 코루틴의 컨텍스트를 상속받으며 새 코루틴의 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: 나는 요청이 취소되어도 영향을 받지 않아
Kotlin
복사
•
코루틴의 부모-자식 관계는 두 가지 방식으로 명시적으로 재정의될 수 있다.
1.
코루틴을 시작할 때 다른 스코프를 명시적으로 지정하면 부모 스코프의 Job을 상속받지 않는다.
2.
새로운 코루틴의 컨텍스트로 다른 Job 객체를 전달하면 부모 스코프의 Job을 재정의한다.
•
두 경우 모두 시작된 코루틴은 시작된 스코프에 묶이지 않고 독립적으로 동작한다.
부모 코루틴의 책임
// 어떤 요청을 처리하기 위해 코루틴을 시작
val request = launch {
repeat(3) { i -> // 여러 자식 작업을 시작
launch {
delay((i + 1) * 200L) // 200ms, 400ms, 600ms의 가변 지연 시간
println("Coroutine $i is done")
}
}
println("request: 나는 끝났고, 여전히 활동 중인 자식들을 명시적으로 join하지 않아도 돼")
}
request.join() // 요청과 그 자식들이 완료될 때까지 기다림
println("이제 요청 처리가 완료되었습니다")
// 출력:
// request: 나는 끝났고, 여전히 활동 중인 자식들을 명시적으로 join하지 않아도 돼
// Coroutine 0 is done
// Coroutine 1 is done
// Coroutine 2 is done
// 이제 요청 처리가 완료되었습니다
Kotlin
복사
•
부모 코루틴은 항상 모든 자식이 완료될 때까지 기다린다.
•
부모 코루틴은 시작한 자식 코루틴들을 명시적으로 추적할 필요가 없으며 자식들이 끝날 때 Job.join을 사용하여 기다릴 필요도 없다.
코루틴 이름 지정하기
log("Started main coroutine")
// 두 개의 백그라운드 값 계산 실행
val v1 = async(CoroutineName("v1coroutine")) {
delay(500)
log("Computing v1")
6
}
val v2 = async(CoroutineName("v2coroutine")) {
delay(1000)
log("Computing v2")
7
}
log("The answer for v1 * v2 = ${v1.await() * v2.await()}")
// 출력:
// [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
Kotlin
복사
•
자동으로 할당된 ID는 코루틴이 자주 로그를 남길 때 유용하며 동일한 코루틴에서 발생한 로그 기록을 연관시키는데 도움이 된다.
•
하지만 특정 요청을 처리하거나 특정 백그라운드 작업을 수행하는 코루틴이라면 디버깅 목적으로 명시적인 이름을 지정하는 것이 더 좋다.
•
CoroutineName 컨텍스트 요소는 스레드 이름과 같은 역할을 하며 디버깅 모드가 켜져 있을 때 이 코루틴을 실행하는 스레드 이름에 포함된다.
컨텍스트 요소 결합
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}
// 출력:
// I'm working in thread DefaultDispatcher-worker-1 @test#2
Kotlin
복사
•
때로는 코루틴 컨텍스트에 여러 요소를 정의해야 할 때가 있다.
•
이를 위해 + 연산자를 사용할 수 있다.
•
예를 들어, 명시적으로 지정된 디스패처와 이름을 동시에 사용하여 코루틴을 시작할 수 있다.
CoroutineScope
•
코루틴의 컨텍스트, 자식, 그리고 작업에 대한 지식을 바탕으로 CoroutineScope를 살펴보자.
class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
// 계속 ...
Kotlin
복사
•
예를 들어, 안드로이드 어플리케이션에서 Activity라는 객체는 코루틴이 아니지만 해당 객체의 수명 동안 데이터를 가져오거나 업데이트하는 비동기 작업, 애니메이션 처리 등 다양한 코루틴을 실행할 수 있다.
•
하지만 Activity가 파괴될 때 이러한 코루틴들이 메모리 누수를 방지하기 위해 함께 취소되어야 한다.
•
물론, 수동으로 컨텍스트와 작업을 관리하여 Activity와 코루틴의 수명을 연결할 수 있다.
•
하지만 kotlinx.coroutines는 이 작업을 추상화한 CoroutineScope를 제공한다.
•
모든 코루틴 빌더는 CoroutineScope의 확장 함수로 선언되므로 CoroutineScope에 익숙할 것이다.
•
우리는 Activity의 수명에 맞춰 코루틴의 수명을 관리하기 위해 CoroutineScope 인스턴스를 생성한다.
•
이 인스턴스는 CoroutineScope() 또는 MainScope() 팩토리 함수로 생성할 수 있다.
•
CoroutineScope는 일반 용도의 스코프를 생성하고 MainScope는 UI 어플리케이션에 적합한 스코프를 생성하며 기본 디스패처로 Dispatchers.Main을 사용한다.
// class Activity 계속
fun doSomething() {
// 데모를 위해 10개의 코루틴 실행, 각각 다른 시간 동안 작업 수행
repeat(10) { i ->
mainScope.launch {
delay((i + 1) * 200L) // 가변 딜레이: 200ms, 400ms, ... 등
println("Coroutine $i is done")
}
}
}
} // class Activity 끝
Kotlin
복사
•
이제 정의된 mainScope를 사용해 이 Activity의 스코프에서 코루틴을 실행할 수 있다.
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!
Kotlin
복사
•
main 함수에서는 Activity를 생성하고 doSomething 함수를 호출한 뒤 500ms 후에 Activity를 파괴하여 모든 코루틴을 취소한다.
•
Activity가 파괴된 후 더 기다려봐도 더 이상 메세지가 출력되지 않는 것을 확인할 수 있다.
안드로이드에서는 모든 생명 주기를 가진 엔티티에 대해 코루틴 스코프에 대한 1급 지원이 제공된다.
Thread-local 데이터
•
때로는 코루틴 간에 스레드 로컬 데이터를 전달하는 것이 편리할 때가 있다.
•
그러나 코루틴은 특정 스레드에 바인딩되지 않기 때문에 이를 수동으로 처리하면 코드가 복잡해질 수 있다.
•
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()}'")
// 출력:
// 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'
Kotlin
복사
•
Dispatchers.Default를 사용하여 백그라운드 스레드 풀에서 새로운 코루틴을 실행한다.
•
이는 스레드 풀에서 다른 스레드에서 실행되지만 코루틴이 실행되는 스레드에 상관없이 스레드 로컬 변수의 값이 threadLocal.asContextElement(value = “launch”)로 지정된 값을 유지한다.
•
스레드 로컬 변수를 설정하는 것을 잊기 쉽다.
•
코루틴이 실행되는 스레드가 다를 경우 스레드 로컬 변수에서 예기치 않은 값을 가질 수 있다.
•
이러한 상황을 피하려면 ensurePresent 메서드를 사용하여 부적절한 사용에 대해 빠르게 실패하도록 하는 것이 좋다.
•
ThreadLocal은 kotlinx.coroutines에서 제공하는 모든 기본 요소와 함께 사용할 수 있다.
•
그러나 중요한 제한 사항이 있는데 컨텍스트 요소는 모든 ThreadLocal 객체 엑세스를 추적할 수 없기 때문에 ThreadLocal이 변경되면 새 값이 코루틴 호출자에게 전달되지 않는다는 점이다.
•
즉, 값이 변경되면 다음 중단점에서 업데이트된 값이 사라진다.
•
코루틴 내에서 ThreadLocal 값을 업데이트하려면 withContext를 사용해야 한다.