일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 쿠링
- activity
- Python
- boj
- pandas
- Coroutine
- Codeforces
- Coroutines
- MyVoca
- MiTweet
- GitHub
- relay
- androidStudio
- Kotlin
- 백준
- Gradle
- livedata
- architecture
- android
- 암호학
- TEST
- AWS
- textfield
- 코루틴
- ProGuard
- Rxjava
- Hilt
- 프로그래머스
- Compose
- 코드포스
- Today
- Total
이동식 저장소
[Kotlin] Coroutines - Context and Dispatchers 본문
목차
코루틴은 항상 CoroutineContext
문맥 안에서 실행된다. 코루틴 문맥에는 여러 원소가 포함되는데, 대표적인 것으로 Job
과 dispatcher가 있다.
Dispatcher와 스레드
코루틴 문맥에는 코루틴이 어느 스레드에서 동작할지를 결정하는 CoroutineDispatcher
가 포함된다. dispatcher는 코루틴이 특정 스레드에서 실행되도록 할 수도 있고, 스레드 풀에서 실행되도록 할 수도 있고, 또는 어느 스레드에서 실행될지 결정하지 않은 상태로 놔 둘 수도 있다. launch
와 async
같은 모든 코루틴 빌더에서 CoroutineContext
매개변수를 지정할 수 있다.
launch { // parent(main)에서 작동
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
코루틴 빌더를 매개변수 없이 사용하면 자신을 실행한 부분의 CoroutineScope
와 CoroutineContext
를 상속받는다. 위의 코드에서는 runBlocking
을 실행한 main
스레드를 상속받았다.
Dispatchers.Unconfined
는 언뜻 보면 옵션을 지정하지 않은 경우와 같아 보이지만, 실제로는 조금 다른 방법으로 작동한다. 밑에서 자세히 설명할 것이다.
Dispatchers.Default
는 GlobalScope
에서 기본값으로 사용되는 dispatcher이다. 따라서 launch(Dispatchers.Default) {...}
는 GlobalScope.launch {...}
와 같은 dispatcher를 사용한다.
newSingleThreadContext
는 코루틴을 실행할 별도의 스레드를 만든다. 사실 이렇게 만들어진 스레드는 매우 무겁기 때문에, 코루틴이 종료된 후 스레드를 close
메소드로 닫던가 아니면 따로 저장해놓고 나중에 재활용하던가 해야 한다.
Unconfined vs. confined
Dispatchers.Unconfined
로 지정된 코루틴은 첫 번째 suspending 함수가 호출되기 전까지만 자신을 실행한 스레드에서 실행된다. 그 후에는 호출된 suspending 함수가 지정한 스레드에서 실행된다. Dispatchers.Unconfined
는 CPU를 적게 사용하면서 특정 스레드에 종속된 공유 데이터에도 접근하지 않는 코루틴에 적합하다.
Dispatcher를 지정하지 않는 경우에는 바깥의 CoroutineScope
가 상속된다. 아래의 예시에서는 runBlocking
코루틴의 dispatcher가 main
스레드에 종속되었기 때문에, 이 dispatcher를 상속받으면 코루틴의 실행이 main
스레드에 종속된다.
launch(Dispatchers.Unconfined) { // main에서 작동되다가 delay 이후에는 다른 스레드에서 작동
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
launch { // runBlocking의 스레드(main)에서 작동
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
Dispatcher.Unconfined
로 지정된 코루틴은 delay
함수가 실행된 이후에는 다른 스레드에서 작동한다. 사실 저 스레드는 delay
함수가 실행된 스레드이다.
다만 코틀린 공식 문서에서는 일반적인 경우 Dispatcher.Unconfined
의 사용을 권장하지 않는다. 원문은 다음과 같다.
The unconfined dispatcher is an advanced mechanism that can be helpful in certain corner cases where dispatching of a coroutine for its execution later is not needed or produces undesirable side-effects, because some operation in a coroutine must be performed right away. The unconfined dispatcher should not be used in general code.
코루틴과 스레드 디버깅
코루틴이 중단된 후 다시 실행될 때 다른 스레드에서 실행될 수도 있다. 물론 하나의 스레드에서만 동작하는 경우도 있지만, 코루틴 특성상 코루틴의 실행 순서 등을 따라가기 쉽지 않다.
IntelliJ IDEA에서 디버깅
kotlinx-coroutines-core 1.3.8
이후 버전을 설치했다면 코루틴 디버거를 사용할 수 있다. 참고로 2021년 1월 21일 기준 최신 stable 버전은 1.4.2
이다.
하단의 Debug 창에 Coroutines 탭이 포함되어 있다. 여기에서 현재 실행 중이거나 중단된 코루틴을 확인할 수 있다.
코루틴 디버거로 다음의 작업을 수행할 수 있다.
- 코루틴의 상태를 확인할 수 있다.
- 코루틴 내부의 변수값을 확인할 수 있다.
- 코루틴이 만들어진 과정(creation stack)과 내부의 call stack을 확인할 수 있다. Stack 안에는 모든 frame의 변수 값이 포함된다.
- 각 코루틴과 코루틴 스택이 포함된 report를 볼 수 있다. 코루틴 탭에서 오른쪽 버튼을 클릭하고 Get Coroutines Dump를 선택하면 된다.
코루틴을 디버깅하려면 중단점을 설정한 후 프로그램을 디버그 모드로 실행해야 한다. 자세한 디버거 활용 방법은 여기에서 확인할 수 있다.
로그로 확인하기
안드로이드 앱을 개발할 때처럼 로그를 출력해 볼 수도 있다. 코루틴 안에서 로그를 찍으면 해당 코루틴의 이름과 코루틴이 실행되고 있는 스레드 이름이 함께 출력된다. 다음의 코드를 -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()}")
[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
로그 메시지 앞에 스레드와 코루틴의 이름이 함께 출력되는 것을 확인할 수 있다.
코루틴에 이름 붙이기
스레드에 이름을 붙일 수 있듯이, 코루틴에도 디버깅 등을 위해 이름을 붙일 수 있다. 이름을 지정하지 않으면 위에서처럼 coroutine#i
형식으로 붙여지며, 이름을 지정할 경우에는 로그에 해당 이름이 출력된다.
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()}")
[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
-Dkotlinx.coroutines.debug
JVM 옵션을 사용하여 실행하면 코루틴 이름이 출력됨을 확인할 수 있다.
문맥 조합하기
지금까지 살펴본 코루틴 문맥의 원소는 dispatcher, 코루틴 이름 등이 있다. 지금까지는 한 번에 하나씩만 사용했지만, +
연산자를 이용하여 여러 원소를 모두 지정할 수 잇다.
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
스레드 간 이동
다음의 코드를 -Dkotlinx.coroutines.debug
JVM 옵션을 주고 실행해 보자.
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
뭔가 새로운 방법이 많이 사용되었다.
runBlocking
을 실행할 때 코루틴 문맥을 지정할 수 있다.withContext
함수를 이용하여 코루틴 안에서 문맥을 바꿀 수 있다. 즉 코루틴이 실행되는 도중 자신이 실행될 문맥을 바꿀 수 있다.- 표준 함수
use
를 사용하여 스레드를 사용한 후 자동으로close
하였다.
Job 확인하기
코루틴 문맥에는 Job
도 포함된다고 했다. 문맥 안의 Job
은 coroutineContext[Job]
을 이용하여 확인할 수 있다.
println("My job is ${coroutineContext[Job]}")
이 코드를 디버깅 모드에서 실행하면 다음의 메시지를 출력한다.
My job is "coroutine#1":BlockingCoroutine{Active}@573fd745
부모-자식 관계
코루틴 안에서 새로운 코루틴을 시작하면 코루틴 문맥이 상속된다고 했다. 이때 새로 시작된 코루틴의 Job
은 자신을 시작한 코루틴의 Job
의 자식이 된다. 부모 코루틴이 종료되면 모든 자식 코루틴도 종료된다.
하지만 GlobalScope
으로 실행된 코루틴은 예외이다. GlobalScope
으로 실행된 코루틴은 어떠한 부모-자식 관계도 갖지 않으며, 자신을 실행한 코루틴과 독립적으로 실행된다.
val request = launch {
// GlobalScope로 실행: request가 종료되어도 계속 실행됨
GlobalScope.launch {
println("job1: I run in GlobalScope and execute independently!")
delay(1000)
println("job1: I am not affected by cancellation of the request")
}
// 자식 코루틴: 부모의 문맥을 상속받아 실행됨
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() // request를 취소하고
delay(1000) // 무슨 일이 벌어지는지 잠시 기다려 보자.
println("main: Who has survived request cancellation?")
job1: I run in GlobalScope and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?
GlobalScope
로 실행된 job1
은 request
가 종료된 후에도 계속 실행되지만, request
의 자식 job2
는 부모가 종료될 때 같이 종료된다.
부(모)성애
부모 코루틴은 모든 자식 코루틴이 완료될 때까지 기다린다. 명시적으로 job.join()
등을 하지 않아도 기다린다.
// request: 부모 코루틴
val request = launch {
repeat(3) { i ->
// 자식 코루틴 실행
launch {
delay((i + 1) * 200L)
println("Coroutine $i is done")
}
}
println("request: I'm done and I don't explicitly join my children that are still active")
}
request.join() // request와 request의 자식이 모두 종료될 때까지 기다림
println("Now processing of the request is complete")
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete
부모 코루틴 request
는 자신의 코드를 모두 실행했지만, 즉시 종료하지 않고 자식 launch
가 모두 종료될 때까지 기다린다.
정리: 코루틴 스코프 (Coroutine scope)
안드로이드 앱을 개발하고 있다고 가정해 보자. 어떤 Activity
가 실행 중일 때 코루틴 여러 개가 실행되어야 한다고 하자. 코루틴은 각각 UI 업데이트, 데이터 로드 등을 담당할 수 있다. 그러나 Activity
와 관련된 코루틴은 메모리 누수를 방지하기 위해 Activity
가 종료될 때 같이 종료되어야 한다. 지금까지 배운 내용으로도 코루틴 각각의 작동을 수동으로 제어할 수는 있다. 하지만 모든 코루틴의 레퍼런스를 저장해 놓고 수동으로 종료하는 방법은 매우 번거로우며 유지보수하기도 쉽지 않다.
kotlinx.coroutines
패키지에 포함된 CoroutineScope
을 사용하면 여러 개의 코루틴을 간편하게 제어할 수 있다. 일반적인 목적의 CoroutineScope
은 CoroutineScope()
로 만들 수 있다. 또는 MainScope()
로 만들 수도 있는데, 이렇게 만들어진 스코프는 Dispatchers.Main
dispatcher를 사용하기 때문에 UI 업데이트 등에 적합하다.
// scope 테스트용, 실제 안드로이드 코드와는 다릅니다.
class TestActivity {
private val mainScope = MainScope()
// ...
fun destroy() {
mainScope.cancel()
}
fun doSomething() {
// 코루틴 10개 실행
repeat(10) { i ->
// mainScope 안에서 코루틴 실행
mainScope.launch {
delay((i + 1) * 200L) // 각기 다른 시간 동안 실행됨
println("Coroutine $i is done")
}
}
}
}
이제 main
함수에서 TestActivity
객체를 만들고, doSomething
함수를 호출한 다음 500ms 후에 activity 객체를 제거할 것이다. 코루틴 스코프에 cancel
명령이 내려지면 스코프에 속한 모든 코루틴이 종료된다.
val activity = Activity()
activity.doSomething() // 코루틴 실행
println("Launched coroutines")
delay(500L) // 잠깐 기다렸다가
println("Destroying activity!")
activity.destroy() // activity 종료: 모든 코루틴 종료
delay(1000) // 모든 코루틴이 종료되었는지 확인
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!
스코프 안의 모든 코루틴이 종료되었음을 알 수 있다.
안드로이드에서 lifecycle이 있는 모든 객체는 자신만의 코루틴 스코프를 갖는다. 자세한 내용은 여기를 참고하자.
참고문헌
'Primary > Kotlin' 카테고리의 다른 글
[Kotlin] Coroutines - Channels (0) | 2021.01.23 |
---|---|
[Kotlin] Coroutines - Asynchronous Flow (1) | 2021.01.22 |
[Kotlin] Coroutines - Suspending 함수 활용하기 (0) | 2021.01.20 |
[Kotlin] Coroutines - Cancellation and Timeouts (0) | 2021.01.20 |
[Kotlin] Coroutines - Basics (0) | 2021.01.19 |