BottleH Blog

Kotlin in Action - 15장 구조화된 동시성

    Tags

  • Kotlin
Kotlin in Action - 15장 구조화된 동시성 thumbnail

📖 15.1 코루틴 스코프가 코루틴 간의 구조를 확립한다

  • 구조화된 동시성을 통해 각 코루틴은 코루틴 스코프에 속하게 된다.
  • 다른 코루틴 빌더의 본문에서 launchasync를 사용해 새로운 코루틴을 만들면 이 새로운 코루틴은 자동으로 해당 코루틴의 자식이 된다.
fun main() { runBlocking { launch { delay(1.seconds) launch { delay(250.milliseconds) log("Grandchild done") } log("Child 1 done!") } launch { delay(500.milliseconds) log("Child 2 done!") } log("Parent done!") } } 0 [main @coroutine#1] Parent done! 524 [main @coroutine#3] Child 2 done! 1020 [main @coroutine#2] Child 1 done! 1275 [main @coroutine#4] Grandchild done
  • 모든 자식 코루틴이 완료될 때까지 프로그램이 종료되지 않는다.

🔖 15.1.1 코루틴 스코프 생성: coroutineScope 함수

  • 코루틴 빌더를 사용해 새로운 코루틴을 만들면 이 코루틴은 자체적인 CoroutineScope를 생성한다.
  • CoroutineScope 함수의 전형적인 사용 사례는 동시적 작업 분해(여러 코루틴을 활용해 계산 수행)
suspend fun generateValue(): Int { delay(500.milliseconds) return Random.nextInt(0, 10) } suspend fun computeSum() { log("Computing a sum...") val sum = coroutineScope { val a = async { generateValue() } val b = async { generateValue() } a.await() + b.await() } log("Sum is $sum") }

🔖 15.1.2 코루틴 스코프를 컴포넌트와 연관시키기: CoroutineScope

  • coroutineScope 함수가 작업을 분해하는 데 사용되는 반면 구체적 생명주기를 정의하고, 동시 처리나 코루틴의 시작과 종료를 관리하는 클래스를 만들고 싶을 대도 잇다.
class ComponentWithScope(dispatcher: CoroutineDispatcher = Dispatchers.Default) { private val scope = CoroutineScope(dispatcher + SupervisorJob()) fun start() { log("Starting") scope.launch { while (true) { delay(500.milliseconds) log("Component working!") } } scope.launch { log("Doing a one-off task...") delay(500.milliseconds) log("Task done!") } } fun stop() { log("Stopping!") scope.cancel() } }
  • Component 클래스의 인스턴스를 생성하고 start를 호출하면 컴포넌트 내부에서 코루틴이 시작된다.

🔖 15.1.3 GlobalScope의 위험성

fun main() = runBlocking { GlobalScope.launch { delay(1000) launch { delay(250) log("Grandchild done") } log("Child 1 done!") } GlobalScope.launch { delay(500) log("Child 2 done!") } log("Parent done!") } 0 [main @coroutine#1] Parent done!
  • GlobalScope는 전역 수준에 존재하는 스코프
  • GlobalScope를 사용하면 구조화된 동시성이 제공하는 모든 이점을 포기
  • 자동취소 불가, 생명주기 개념 없음

🔖 15.1.4 코루틴 콘텍스트와 구조화된 동시성

fun main() { runBlocking(Dispatchers.Default) { log(coroutineContext) launch { log(coroutineContext) launch(Dispatchers.IO + CoroutineName("mine")) { log(coroutineContext) } } } } 0 [DefaultDispatcher-worker-1 @coroutine#1] [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@3a617b0f, Dispatchers.Default] 13 [DefaultDispatcher-worker-2 @coroutine#2] [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@5e2dba97, Dispatchers.Default] 14 [DefaultDispatcher-worker-3 @mine#3] [CoroutineName(mine), CoroutineId(3), "mine#3":StandaloneCoroutine{Active}@12764f6a, Dispatchers.IO]
  • 자식 코루틴은 부모의 콘텍스트 상속
  • 새로운 코루틴은 부모-자식 관계 설정하는 역할을 하는 새 Job 객체 생성
  • 디스패처를 지정하지 않고 새로운 코루틴을 시작하면 부모 코루틴의 디스패처에서 실행
fun main() = runBlocking(CoroutineName("A")) { log("A's job: ${coroutineContext.job}") launch(CoroutineName("B")) { log("B's job: ${coroutineContext.job}") log("B's parent: ${coroutineContext.job.parent}") } log("A's children: ${coroutineContext.job.children.toList()}") }
  • 코루틴 간의 부모-자식 관계를 확인할 수 있음
  • 구조화된 동시성에 의해 설정된 이 부모-자식 관계는 취소와도 연관이 있다.

📖 15.2 취소

  • 취소는 코드가 완료되기 전에 실행을 중단하는 것을 의미
  • 취소는 불필요한 작업을 막아준다.
  • 취소는 메모리나 리소스 누수 방지에 도움을 준다.
  • 취소는 오류 처리에서도 중요한 역할을 한다.

🔖 15.2.1 취소 촉발

fun main() = runBlocking { val launchedJob = launch { log("I'm launched!") delay(1000.milliseconds) log("I'm done!") } val asyncDeferred = async { log("I'm async") delay(1000.milliseconds) log("I'm done!") } delay(200.milliseconds) launchedJob.cancel() asyncDeferred.cancel() } 0 [main @coroutine#2] I'm launched! 11 [main @coroutine#3] I'm async
  • cancel을 호출해 해당 코루틴의 취소를 촉발할 수 있다.

🔖 15.2.2 시간제한이 초과된 후 자동으로 취소 호출

suspend fun calculateSomething(): Int { delay(3.seconds) return 2 + 2 } fun main() = runBlocking { val quickResult = withTimeoutOrNull(500.milliseconds) { calculateSomething() } println(quickResult) // null val slowResult = withTimeoutOrNull(5.seconds) { calculateSomething() } println(slowResult) // 4 }
  • withTimeout, withTimeoutOrNull 함수는 계산에 쓸 최대 시간을 제한하면서 값을 계산할 수 있게 해준다.

🔖 15.2.3 취소는 모든 자식 코루틴에게 전파된다

fun main() = runBlocking { val job = launch { launch { launch { launch { log("I'm started") delay(500.milliseconds) log("I'm done!") } } } } delay(200.milliseconds) job.cancel() }
  • 코루틴을 취소하면 해당 코루틴의 모든 자식 코루틴도 자동으로 취소된다.
  • 여러 계층에 걸쳐 코루틴이 중첩돼 있는 경우에도 가장 바깥쪽 코루틴을 취소하면 고손자 코루틴까지 모두 적절히 취소된다.

🔖 15.2.4 취소된 코루틴은 특별한 지점에서 CancellationException을 던진다

  • 취소 메커니즘은 CancellationException이라는 특수한 예외를 특별한 지점에서 던지는 방식으로 작동
  • 취소된 코루틴은 일시 중단 지점에서 CancellationException을 던진다.
  • 코루틴 계층에서 취소를 전파하기 때문에 이 예외를 직접 처리하지 않아야 한다.

🔖 15.2.5 취소는 협력적이다

suspend fun doCpuHeavyWork(): Int { log("I'm doing work!") var counter = 0 val startTime = System.currentTimeMillis() while (System.currentTimeMillis() < startTime + 500) { counter++ } return counter } fun main() = runBlocking { val myJob = launch { repeat(5) { doCpuHeavyWork() } } delay(600.milliseconds) myJob.cancel() }
  • 프로그램이 종료되기 전에 doCpuHeavyWork가 5번 완료된다.
  • doCpuHeavyWork 함수는 일시 중단 지점을 포함하지 않는다.
  • 코틀린 코루틴의 취소가 협력적인 이유는 결국 스스로 취소 가능하게 로직을 제공해야 하기 때문
    • delay 호출을 추가하면 됨.

🔖 15.2.6 코루틴이 취소됐는지 확인

val myJob = launch { repeat(5) { doCpuHeavyWork() if (!isActive) return@launch } }
  • 코루틴이 취소됐는지 확인할 때는 isActive 속성을 확인
    • false이면 비활성화
  • ensureActive 함수는 비활성화일 때, CancellationException을 던진다.

🔖 15.2.7 다른 코루틴에게 기회를 주기: yield 함수

  • 코루틴 라이브러리는 yield 함수 제공
    • 취소 가능 지점 제공
    • 점유된 디스패처에서 다른 코루틴이 작업할 수 있게 해줌
fun doCpuHeavyWork(): Int { var counter = 0 val startTime = System.currentTimeMillis() while (System.currentTimeMillis() < startTime + 500) { counter++ } return counter } fun main() { runBlocking { launch { repeat(3) { doCpuHeavyWork() } } launch { repeat(3) { doCpuHeavyWork() } } } }
  • 첫 번째 코루틴이 완료될 때까지 두 번째 코루틴은 실행되지 않음.
  • 일시 중단 지점이 없기 때문
suspend fun doCpuHeavyWork(): Int { var counter = 0 val startTime = System.currentTimeMillis() while (System.currentTimeMillis() < startTime + 500) { counter++ yield() } return counter } fun main() { runBlocking { launch { repeat(3) { doCpuHeavyWork() } } launch { repeat(3) { doCpuHeavyWork() } } } }
  • yield 함수를 사용하면 코루틴이 교차 실행됨

🔖 15.2.8 리소스를 얻을 때 취소를 염두에 두기

  • 취소 후, close 함수가 호출되지 않고 리소스가 누수될 수 있음.
  • finally 블록을 사용해 명시적으로 닫자.
  • 리소스가 AutoClosable 인터페이스를 구현하는 경우 .use 함수를 사용해 같은 동작을 더 간결하게 처리할 수 있다.

🔖 15.2.9 프레임워크가 여러분 대신 취소를 할 수 있다

  • 많은 실제 어플리케이션에서는 프레임워크가 코루틴 스코프를 제공하고 자동취소한다.
  • 사용자는 적절한 코루틴 스코프를 선택
Written by@BottleH
Back-End Developer

GitHub