BackEnd/coroutine

Coroutine 취소

hanseom 2025. 6. 1. 05:00
반응형

Coroutine 취소

  코루틴 실행 도중, 더이상 코루틴을 실행할 필요가 없어지면 즉시 취소해야 합니다. 취소하지 않으면 코루틴이 스레드를 계속해서 사용하기 때문에 애플리케이션의 성능 저하로 이어집니다.

  • 예) 사용자가 오래 걸리는 이미지 변환 작업을 요청한 후 취소한 경우

 

  다음은 2500밀리초 이후 longJob 코루틴을 취소하는 코드입니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val longJob: Job = launch(Dispatchers.Default) {
        repeat(10) { repeatTime ->
            delay(1000L) // 1000밀리초 대기
            println("[${getElapsedTime(startTime)}] 반복횟수 ${repeatTime}")
        }
    }
    delay(2500L) // 2500밀리초 대기
    longJob.cancel() // 취소
}

fun getElapsedTime(startTime: Long): String =
    "지난 시간: ${System.currentTimeMillis() - startTime}밀리초"

 

cancel 함수

  • cancel 함수는 코루틴을 곧바로 취소하지 않습니다.
  • 취소 확인용 플래그를 '취소 요청됨'으로 바꾸는 역할만 합니다.
  • 이후 취소 확인용 플래그가 확인되는 시점에 코루틴이 취소됩니다.
  • cancel 함수를 사용하는 것은 순차성 관점에서 중요한 문제를 가집니다.

 

  다음은 longJob 취소 요청 후 executeAfterJobCancelled 함수를 호출하는 예제 코드입니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val longJob: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println("longJob 코루틴의 동작")
    }
    longJob.cancel() // longJob 취소 요청
    executeAfterJobCancelled() // 취소 후 실행돼야 하는 동작
}

fun executeAfterJobCancelled() {
    println("longJob 코루틴 취소 후 실행돼야 하는 동작")
}

 

  cancel 함수를 호출하였음에도 longJob이 취소되지 않고, 다음과 같은 결과를 출력합니다.

[출력]
longJob 코루틴 취소 후 실행돼야 하는 동작
longJob 코루틴의 동작

 

  코루틴 라이브러리는 취소의 순차성을 보장하기 위해 cancelAndJoin 함수를 제공합니다.

 

cancelAndJoin 함수

  코루틴이 취소된 후 실행돼야 하는 코루틴이 있다면, 코루틴을 취소할 때 cancelAndJoin 함수를 사용하면 됩니다.

  • cancelAndJoin = Cancel + Join: 취소 요청한 후 취소가 완료될 때까지 호출 코루틴 일시 중단
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val longJob: Job = launch(Dispatchers.Default) {
        Thread.sleep(1000L)
        println("longJob 코루틴의 동작")
    }
    longJob.cancelAndJoin() // longJob 취소 요청 후 취소 완료될 때까지 호출 코루틴 일시 중단
    executeAfterJobCancelled() // 취소 후 실행돼야 하는 동작
}

fun executeAfterJobCancelled() {
    println("longJob 코루틴 취소 후 실행돼야 하는 동작")
}
[출력]
longJob 코루틴의 동작
longJob 코루틴 취소 후 실행돼야 하는 동작

 

  longJob 코루틴을 실행하자마자 취소하였음에도 취소되지 않는 이유는 코루틴의 취소 확인 시점이 없기 때문입니다. 코루틴은 기본적으로 일시중단 시점에 코루틴을 취소하는데, Thread.sleep 함수는 스레드를 블로킹 하기 때문입니다. 다음과 같이 Thread.sleep 함수를 delay 함수로 변경하고 실행하면 됩니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val longJob: Job = launch(Dispatchers.Default) {
        delay(1000L)
        println("longJob 코루틴의 동작")
    }
    longJob.cancelAndJoin() // longJob 취소 요청 후 취소 완료될 때까지 호출 코루틴 일시 중단
    executeAfterJobCancelled() // 취소 후 실행돼야 하는 동작
}

fun executeAfterJobCancelled() {
    println("longJob 코루틴 취소 후 실행돼야 하는 동작")
}
[출력]
longJob 코루틴 취소 후 실행돼야 하는 동작

 

Thread.sleep() vs delay()

  • Thread.sleep(): 코루틴의 suspend 함수가 아니고, 단순히 현재 스레드를 블로킹합니다.
  • delay(): 코루틴 suspend 함수로, 내부적으로 취소 신호를 체크합니다. 취소되면 즉시 CancellationException을 던지고 코루틴을 중단합니다.

 

Coroutine 취소 확인

  코루틴이 취소를 확인하는 시점은 다음 두가지입니다.

  1. 일시 중단 시점
  2. 코루틴이 실행을 대기하는 시점

 

  다음 코드는 취소 확인 시점이 없어 "작업 중"을 계속 출력합니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println("작업 중")
        }
    }
    delay(100L) // 100밀리초 대기
    whileJob.cancel() // 코루틴 취소
}
[출력]
작업 중
작업 중
...
작업 중
작업 중
...
(무한 루프)

 

코루틴에 취소 확인 시점을 만드는 세가지 방법

  1. delay 함수
  2. yield 함수
  3. CoroutineScope.isActive

 

delay 함수

  delay 함수는 특정 시간만큼 코루틴을 일시 중단하게 만듭니다. 단, delay 함수를 사용해 취소를 할 경우 불필요하게 작업을 지연시켜 성능 저하가 일어납니다. 다음 코드에서는 매 반복마다 1밀리초만큼 지연이 발생합니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println("작업 중")
            delay(1L)
        }
    }
    delay(100L)
    whileJob.cancel()
}

 

yield 함수

  yield 함수를 호출한 코루틴은 자신이 사용하던 스레드를 양보합니다. 양보한다는 것은 코루틴이 스레드 사용을 일시 중단 한다는 뜻입니다. 스레드를 양보한 후 곧바로 재개 요청됩니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(true) {
            println("작업 중")
            yield()
        }
    }
    delay(100L)
    whileJob.cancel()
}

 

  delay 함수와 yield 함수는 모두 일시중단 후 재개 과정을 거칩니다. 재개 시에는 CoroutineDispatcher에 의해 다시 스레드로 보내지는 과정을 거치기 때문에 비효율적입니다.

 

CoroutineScope.isActive

  CoroutineScope의 isActive 프로퍼티를 사용하면 코루틴에 취소가 요청됐는지 확인할 수 있습니다. 코루틴에 취소가 요청되면, CoroutineScope.isActive가 false가 됩니다. CoroutineScope 객체의 isActive 확장 프로퍼티를 사용하면 코루틴을 일시 중단 시키지 않고 취소를 확인할 수 있습니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    val whileJob: Job = launch(Dispatchers.Default) {
        while(this.isActive) {
            println("작업 중")
        }
    }
    delay(100L)
    whileJob.cancel()
}

 

반응형