본문 바로가기

Coroutine

Coroutines - Part. 7 Debugging Coroutine and Thread

728x90

코루틴은 한 스레드에 의해 중지되고 다른 스레드에서 다시 시작이 가능하다. Single Thread Dispatcher를 사용할 경우 코루틴이 언제, 어디서, 무엇을 수행하는지 파악하기가 쉽지 않다.

 

Debugging using logging

각 로그문장에 스레드의 이름을 출력하는 것이 가장 일반적인 방법이지만, 코루틴을 사용할 때 스레드의 이름만으로는 context를 판단하기는 어렵기 때문에 kotlinx.coroutines에서 지원하는 디버깅을 사용하는 것이 좋다.

 

아래의 예제를 실행해보자

 

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
    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

위 코드를 보면 runBlocking 내부의 메인 코루틴(#1)과 defferred 값인 a(#2), b(#3)까지 총 3개의 코루틴이 작업을 하게된다. 이 3개의 코루틴 모두 runBlocking이라는 context에서 실행되며, 메인 스레드로 제한되어 있다.

 

아래 예제를 실행해보자

 

Jumping between threads

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

위 코드를 보면 지금까지 해왔던, 봐왔던 예제랑은 조금 다르게 보이지만, Part. 6에서 공부했었던 newSingleThreadContext() 메서드를 이용하여 실행시킬 Ctx1, Ctx2 Thread를 생성했고, 명시적으로 지정된 context의 runBlocking을 사용하며 내부에서 withContext() 메서드를 이용하여 코루틴의 context만 변경했다.

Thread는 달라졌지만, 결국 하나의 코루틴 블럭을 머무르고 있다.

 

newSingleThreadContext() 메서드로 생성한 Thread를 더 이상 사용하지 않게되면 해제해줘야 하는데, 이 경우 use 함수를 사용했는 것에 주목하자고 한다.

 

use() : 예외 발생 시 리소스를 해제해야할 때 쓰는 것으로 알고있다.

 

Job in the context

코루틴의 Job은 context의 일부로 coroutineContext[Job] 구문을 이용해 검색이 가능하다.

 

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")
}

실행결과:

My job is "coroutine#1":BlockingCoroutine{Active}@4e515669

참고로 CoroutineScope의 isActive는 coroutineContext[Job]?.isActive == true의 단축키라는 것이라고 한다.

 

Children of a coroutine

코루틴이 다른 코루틴의 CoroutineScope에서 실행되면 CoroutineScope.Context로 인해 context를 이어받게 된다고 한다. 새로운 코루틴의 Job은 부모 코루틴의 Job이 되는 것이며, 부모 코루틴이 취소된다면 모든 하위 코루틴들도 쥐소된다.

 

하지만 GlobalScope를 사용하여 코루틴을 시작한 경우에는 다르다. GlobalScope는 시작된 범위와 관련이 없이 독립적으로 작동한다.

import kotlinx.coroutines.*


fun main() = runBlocking<Unit> {
    // 어떤 종류의 요청을 처리하기 위해 코루틴 launch
    val request = launch {
        // GlobalScope와 함께 두 개의 다른 job을 생성합니다.
        GlobalScope.launch {
            println("job1 : GlobalScope에서 실행하고 독립적으로 실행합니다!!")
            delay(1000)
            println("job1 : 요청 취소의 영향을받지 않음")
        }
        // 그리고 다른 하나는 상위 context를 상속받습니다.
        launch {
            delay(100)
            println("job2 : 요청 코 루틴의 자식입니다.")
            delay(1000)
            println("job2 : 부모 요청이 취소되면 이 줄을 실행하지 않습니다")
        }
    }
    delay(500)
    request.cancel() // request 취소 처리.
    delay(1000) // 어떤 일이 발생하는지 보려고 딜레이
    println("main : 요청 취소에서 누가 살아 남았습니까?")
}

실행결과:

job1 : GlobalScope에서 작업하고 독립적으로 실행합니다!!
job2 : 요청 코루틴의 자식입니다.
job1 : 요청 취소의 영향을 받지 않는다
main : 요청 취소에서 누가 살아 남았습니까?

위 코드의 실행결과를 보면 GlobalScope 코루틴은 request 코루틴 내부에 존재하지만 취소되지 않고 독립적으로 작업하고 있다는 것을 알 수 있다.

 

Parental responsibilities

부모 코루틴은 항상 모든 자식의 코루틴들이 완료되기를 기다리기 때문에 자식 코루틴에 대한 join() 명령이 필요가 없고, 모든 자식 코루틴들을 명시적으로 알고 있을 필요도 없다.

import  kotlinx.coroutines. *

fun main() = runBlocking<Unit> {
    // sampleStart
    // 들어오는 요청을 처리하기 위해 코 루틴을 시작합니다.
    val request = launch {
        
        repeat(3) { i -> // 몇 가지 하위 작업 시작
            launch  {
                delay((i + 1) * 200L) // 가변적인 지연 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // 모든 자식 코루틴들을 포함하여 그것의 완료를 기다린다.
    println("Now processing of the request is complete")
    // sampleEnd
}

실행결과: 

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

실행결과를 보면 위에서 설명한 것처럼 부모 코루틴에 대한 join() 메서드를 사용한 것으로 자식 코루틴의 작업까지 대기한 후 루틴을 마치는 것을 확인할 수 있다.