코루틴 마지막 글에서는 CoroutinScope, CoroutineContext, Dispatchers에 대해서 작성해보겠습니다.
launch{} 는 코루틴을 생성하는 Coroutine Builder입니다. launch내에서 람다 표현식을 사용하면 CoroutineScope라고 하는 키워드가 나타나게 됩니다. 짧게 정리하자면 각각의 코루틴은 자신은 코루틴 인스턴스에 attached 된다는 것입니다.
같은 Coroutine builder인 async와 runBlocking 또한 마찬가지 입니다.
코드로 더 설명해드리겠습니다.
fun main() = runBlocking { //this : CoroutineScope
println("runBlocking : $this")
println("some other code...")
}
다음과 같이 runBlocking을 선언한다면 this : CoroutineScope라는 글자가 나타나게 되고 해당 코드를 실행하게 되면 결과는 다음과 같습니다.
runBlocking : BlockingCoroutine{Active}@2077d4de
some other code...
this라는 것을 출력하게되면 Blocking의 Coroutine이 나오게되는데, Active라는 되어있는 것은 Coroutine이 executed되어 동작했다라는 것을 알려주는 것이고, 그 뒤의 숫자 영어들은 hash code를 HEX 값의 형태로 출력한 것입니다.
위의 코드에서 async와 launch를 추가해보겠습니다.
fun main() = runBlocking { //this : CoroutineScope
println("runBlocking : $this")
launch {
println("launch : $this")
}
async {
println("async : $this")
}
println("some other code...")
}
그리고 동작을 시킨다면
runBlocking : BlockingCoroutine{Active}@2077d4de
some other code...
launch : StandaloneCoroutine{Active}@51521cc1
async : DeferreCoroutine{Active}@1b4f997
다음과 같이 launch는 StandaloneCoroutine이라는 Coroutine Scope를 생성하고 async는 DeferreCoroutine Scope를 생성하게 됩니다. 기본적으로 Coroutine Scope는 어떤 종류의 Coroutine이 생성되었는지를 표현해주는 것입니다.
이번에는 부모 코루틴에서 자식 코루틴을 생성하게 된다면 Coroutine Scope는 어떻게 되는지 알아보겠습니다.
fun main() = runBlocking { //this : CoroutineScope
println("runBlocking : $this")
launch {
println("launch : $this")
launch {
println("child launch : $this")
}
}
async {
println("async : $this")
}
println("some other code...")
}
다음과 같이 lauch 코루틴 안에 자식 코루틴을 선언해보았습니다. 결과는 다음과 같이 나옵니다.
runBlocking : BlockingCoroutine{Active}@2077d4de
some other code...
launch : StandaloneCoroutine{Active}@51521cc1
async : DeferreCoroutine{Active}@1b4f997
child launch : StandaloneCoroutine{Active}@deb6432
자식 코루틴의 HEX 값이 다르다는 것을 통해 각각의 코루틴은 자신만의 Coroutine Scope를 가진다는 것을 알게되었습니다.
CoroutineScope의 경우 각각의 코루틴 마다 고유의 CoroutineScope를 가진다는 알게되었습니다. 그러나 CoroutineContext의 경우 부모에서 자식으로 상속이 가능합니다. 이말은 자식 코루틴은 상황에 따라 부모의 코루틴을 상속받아 적절하게 사용할 수 있다는 것입니다.
CoroutineContext의 주요 컴포넌트 2개에 대해 알아보겠습니다.
Dispather : Dispatcher의 경우 코루틴이 잘동작할 수 있도록 thread를 결정해주는 역할을 합니다.
Job : Job의 경우 그 전에 Coroutine 글에서 알 수 있듯이 Coroutine의 동작을 제어해주는데 사용할 수있습니다.
글보다는 코드를 통해 더 쉽게 이해해보겠습니다.
fun main() = runBlocking { //thread : main
launch {
println("C1 : $Thread.currentThread().name}") // main
}
println("Main Program...")
}
해당 프로그램을 동작시키게 된다면 결과는 다음과 같습니다.
Main Program...
C1 : main
해당 결과는 runBlocking Coroutine이 Main Thread에서 동작을 하기에 자식 코루틴인 launch 또한 Main에서 동작을 한 것입니다.
fun main() = runBlocking { //thread : main
//파라미터를 사용하지 않는다면
launch {
println("C1 : $Thread.currentThread().name}") // main
}
/ Default 파라미터를 사용한다면
launch(Dispatchers.Default) {
println("C2 : $Thread.currentThread().name}") // worker-1
delay(1000)
println("C2 : $Thread.currentThread().name}") // worker-1
}
println("Main Program...")
}
결과는 다음과 같이 동작합니다.
Main Program...
C2 : DefualtDispatcher-worker-1
C1 : main
C2의 경우는 Background Thread에서 동작을 하게 되었습니다.
launch(Dispatchers.Default) {
println("C2 : $Thread.currentThread().name}") // worker-1
delay(1000)
println("C2 : $Thread.currentThread().name}") // worker-1 or some other thread
}
delay를 주고 다시 println를 통해 Coroutine을 동작하게 된다면 해당 동작을 수행하는 코루틴은 worker-1 또는 다른 스레드가 하게될 것입니다. Dispatchers.Default의 경우 GlobalScope.launch와 비슷하게 동작하기 때문입니다.
다음은 Dispatchers.Unconfined를 사용해보겠습니다.
fun main() = runBlocking { //thread : main
//파라미터를 사용하지 않는다면
launch {
println("C1 : $Thread.currentThread().name}") // main
}
/ Default 파라미터를 사용한다면
launch(Dispatchers.Default) {
println("C2 : $Thread.currentThread().name}") // worker-1
delay(1000)
println("C2 : $Thread.currentThread().name}") // worker-1 or some other thread
}
launch(Dispatchers.Unconfined) {
println("C3 : $Thread.currentThread().name}") // main
delay(100)
println("C3 after delay : $Thread.currentThread().name}") // some other thread
}
println("Main Program...")
}
unconfined의 뜻은 정의하지 않았다는 것입니다. 그렇기에 동작에 있어서는 부모의 코루틴이 바로 수행이 가능한 경우에는 부모의 코루틴을 가져와 사용을 하게 되고 delay와 같은 대기가 생긴다면 바로 Background Thread를 통해 나머지 동작을 수행하게 해줍니다.
결과는 다음과 같이 나옵니다.
C2 : DefualtDispatchers-worker-1
C3 : main
Main Program...
C1 : main
c3 after delay : kotlinx.coroutines.DefaultExecutor
c3 after delay : DefaultDispatcher-worker-1
Dispatchers를 통해 어떠한 Thread에서 동작할 것인지 하는 것을 지정해줄 수있습니다. 이제 coroutineContext를 사용해보겠습니다.
fun main() = runBlocking { //thread : main
//파라미터를 사용하지 않는다면
launch {
println("C1 : $Thread.currentThread().name}") // main
delay(100)
println("C1 : $Thread.currentThread().name}") // main
}
/ Default 파라미터를 사용한다면
launch(Dispatchers.Default) {
println("C2 : $Thread.currentThread().name}") // worker-1
delay(1000)
println("C2 : $Thread.currentThread().name}") // worker-1 or some other thread
}
launch(Dispatchers.Unconfined) {
println("C3 : $Thread.currentThread().name}") // main
delay(100)
println("C3 after delay : $Thread.currentThread().name}") // some other thread
}
launch(CoroutineContext) {
println("C4 : $Thread.currentThread().name}") // main
delay(1000)
println("C4 after delay : $Thread.currentThread().name}") // main
}
println("Main Program...")
}
CoroutineContext의 경우에는 부모의 Coroutine여기서는 runBlocking의 코루틴을 가져와 사용하기에 main thread에서 동작하게 됩니다. 그렇기에 delay를 하고난 후에도 main에서도 동작합니다. 결과를 보겠습니다.
C2 : DefualtDispatchers-worker-1
C3 : main
Main Program...
C1 : main
C4 : main
C3 after delay : kotlinx.coroutines.DefaultExecutor
C3 after delay : DefaultDispatcher-worker-1
C1 : main
C4 after delay : main
CoroutineContext의 위치를 바꿔보겠습니다.
fun main() = runBlocking { //thread : main
//파라미터를 사용하지 않는다면
launch {
println("C1 : $Thread.currentThread().name}") // main
delay(100)
println("C1 : $Thread.currentThread().name}") // main
}
/ Default 파라미터를 사용한다면
launch(Dispatchers.Default) {
println("C2 : $Thread.currentThread().name}") // worker-1
delay(1000)
println("C2 : $Thread.currentThread().name}") // worker-1 or some other thread
}
launch(Dispatchers.Unconfined) {
println("C3 : $Thread.currentThread().name}") // T1
delay(100)
println("C3 after delay : $Thread.currentThread().name}") // T1
launch(CoroutineContext) {
println("C4 : $Thread.currentThread().name}") // T1
delay(1000)
println("C4 after delay : $Thread.currentThread().name}") // T1
}
}
println("Main Program...")
}
기존에 부모의 Coroutine의 경우 runBlocking이였지만 이렇게 수정한다면 launch(Dispatchers.Unconfined)가 부모 Coroutine이 되는 것입니다. 그렇기에 결과에도 변화가 생기게됩니다.
C2 : DefualtDispatchers-worker-1
C3 : main
Main Program...
C1 : main
C3 after delay : kotlinx.coroutines.DefaultExecutor
C4 : kotlinx.coroutines.DefaultExecutor
C3 after delay : DefaultDispatcher-worker-1
C1 : main
C4 after delay : kotlinx.coroutines.DefaultExecutor
다음과 같이 말입니다.
Dispatcher뒤에 붙일 수 있는 함수는 4개가 있습니다.
Default
Unconfined
위의 2개는 코드로 설명이 되었고
Main : Main의 경우 UI 동작에 있어 처리하는 Coroutine의 경우 Main을 붙혀주게 됩니다. 동작하게 되는 스레드는 Main(UI) Thread에서 동작하게 됩니다.
IO : IO의 경우 Background Thread를 생성해주는데 주로 무거운 작업(네트워크 통신, 대용량 데이터베이스의 데이터 저장 및 호출, 많은 데이터의 수학적 처리)에 주로 사용이 됩니다. 이런 무거운 작업을 IO가 아닌 Main을 사용하여 동작을 하게 된다면 ANR이 발생하여 APP이 동작을 중지하게 됩니다.
코드의 설명으로는 Default와 Unconfined를 사용하였지만 실제로 개발에서는 Main과 IO를 주로 사용하게 됩니다.
이상으로 5개의 챕터로 된 Coroutine 글작성을 마치도록 하겠습니다. 공부를 더 하면서 알게되는 Coroutine의 동작이나 앞으로 추가적으로 공부할 예정 flow에 대해서도 작성해보도록 하겠습니다. 읽어주셔서 감사합니다.