카샤의 만개시기

Kotlin Coroutine(코루틴) - 문맥(Context)과 제어 본문

Kotlin

Kotlin Coroutine(코루틴) - 문맥(Context)과 제어

SKaSha 2019. 12. 13. 22:33

코루틴의 문맥

코루틴은 항상 특정 문맥에서 실행되는데 이런 문맥은 CoroutineContext에 의해 정의되고 내부적으로 CommonPool이 지정되어 코루틴이 사용할 스레드의 공동 풀을 사용하게 됩니다.
코루틴은 이 pool에 이미 초기화되어 있는 스레드 하나 이상을 선택하여 초기화하기 때문에 스레드를 생성하는 오버헤드가 적고 하나의 스레드에 다수의 코루틴을 지정할수 있어 성능이 빠릅니다.
특정 스레드 개수를 지정하려면 다음과 같이 사용자 문맥을 지정해 만들수 있습니다.

val threadPool = Executors.newFixedThreadPool(4)
val myContext = threadPool. asCoroutineDispatcher()

async(myContext) { ... }

코루틴이 어떤 컨텍스트에서 실행하는지 여부는 디스패쳐(Dispatcher)가 결정합니다.
CoroutineDispatcher는 추상 클래스로 몇가지 디스패처 객체를 정의하고 있습니다.

기본 문맥

Dispatchers.Default(GlobalScope)는 기본 문맥인 CommonPool에서 실행되기 때문에 새로운 스레드를 생성하지 않고 기존에 있는 것을 이용합니다.
그러므로 연산 중심의 코드에 적합합니다.

I/O를 위한 문맥

입출력에 적합한 공유 풀로써, 볼로킹 동작이 많은 파일 or 소켓 I/O 처리에 사용하면 좋습니다.

Unconfined 문맥

비한정 문맥에서 실행된 코루틴은 첫번째 중단점을 만날때까지만 호출자 스레드에서 실행됩니다.
중단점 이후에 재개 되었을때는 서스펜드 함수가 실행된 스레드에서 수행됩니다.
그렇기 때문에 비한정 문맥은 예측 불능한 상태로 수행되기 때문에 해당 기능을 사용하는 것은 권장되지 않습니다.

코루틴이 특정 스레드에 국한된 작업이 아닐 경우 사용되며 코루틴의 작업이 즉시 수행되어야 할때 도움이 됩니다.

새 스레드를 생성하는 문맥

newSingleThreadContext는 새로운 스레드가 생성되기 때문에 비용이 많이 들고 더 이상 필요하지 않으면 해제하거나 종료시켜야 합니다.
부모 코루틴이 취소되는 경우 자식 코루틴도 재귀적으로 취소됩니다.
따라서 필요한 경우 join()함수를 사용하여 명시적으로 처리를 기다리도록 만들 수 있습니다.

start 매개변수

코루틴은 문맥 매개변수 이외에 start 매개변수를 지정할 수 있는데 CoroutineStart는 다음과 같이 시작 방법을 지정할수 있습니다.

  • DEFAULT: 즉시 시작
  • LAZY: 코루틴을 느리게 시작 (처음에는 중단된 상태이며 start()나 await() 등으로 시작)
  • ATOMIC: 최적화된 방법으로 시작
  • UNDISPATCHED: 분산 처리 방법으로 시작
// LAZY 예제
val job = async(start = CoroutineStart.LAZY) { doWork() }
job.start()

finally를 통한 코루틴 실행 보장

val job = launch {
  try {
   repeat(100) { i -> println("hello") }
  } finally {
      println("bye")
  }
}
delay(3000L)
job.cancelAndJoin() //작업을 취소하고 완료될 때까지 기다림

취소 가능한 중단함수들을 취소하면 CancellationException 이 발생되는데 예외처리를 통하여 이를 처리할수 있습니다.
만약 예외발생 시, 해제해야 하는 리소스가 있다면 finally 키워드를 이용하여 실행을 보장 받을수 있습니다.
finally 블록에서는 지연 함수를 사용하려고 하면 코루틴이 취소되므로 지연 함수를 사용해서는 안됩니다.
만약 시간이 걸리는 작업이나 지연함수가 사용되어야 한다면 NonCancellable 문맥에서 작동하도록 해야합니다.

try {

} fianally {
    withContext(NonCancellable) {
        println("nonCancellable")
        delay(1000L)
        println("지연 함수를 이용하여 1초 지연하여도 취소되지 않음!")
    }
}

use를 이용하여 리소스 해제

try ~ finally키워드를 사용하여 리소스를 해제하는 방법 대신에 use함수를 이용하여 리소스를 해제 할 수도 있습니다.

fun main(args: Array<String>) = runBlocking {
    val job = launch {
        Resource().use {
            it.read()
        }
    }
    ....
}

class Resource : Closeable {
    suspend fun read() {
        println("resource read")
    }

    override fun close() {
        println("resource closed")
    }
}

조건식에 따른 실행 상태

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val job = GlobalScope.launch {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // 조건을 계산에 의해 반복
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    println("main: I'm tired of waiting!")
    job.cancelAndJoin()
    println("main: Now I can quit.")
}

코루틴에 조건식을 넣었을때 연산이 마무리 되기 전까지는 루틴이 중단되지 않습니다.
위 예제에서는 1.3초 이후 작업 취소 함수에 의해 시그널을 받아 루틴이 취소될것 같지만 while문이 종료되기 전까지 루틴은 끝나지 않습니다.
취소 시그널을 받아 루프를 종료하면 while(isActive)로 변경합니다.

참조

서적 : Do it! 코틀린 프로그래밍

Comments