IT/Kotlin

[Kotlin/Coroutine] 6. Coroutine의 Structured Concurrency(구조화된 동시성)

Hodie! 2023. 5. 19. 20:32
반응형

Structured Concurrency(구조화된 동시성)

  • 누가 기존의 동작을 취소할 수 있는가?
    해당 동작은 Application의 LifeCycle을 충분히 준수하는가?
    예외가 발생하면 이를 누가 처리하는가?
    → 이에 대한 물음을 해소하기 위해 CoroutineStructed Concurrency를 내세운다.
  • 이 개념을 베이스로 만들어진, Scope, launchasync 블록을 사용하면,
    개발자는 메모리 누수 및 동시성을 가진 Function들을 올바르게 처리할 수 있게 된다.

예시

  • GlobalScope.launchtop-level Coroutine을 만든다.
    • 비록 Light-Weight이기는 하나, 여전히 Memory를 비롯한 Resource를 사용한다.
      → 새롭게 luanchcoroutine이 돌고있다는 사실을 잊는다면, GloabalScopeprocess가 살아 있는 한, 계속 유지되기 때문에 로직이 계속 돌것이다.
    • GlobalScope에 계속해서 launch하는 작업은, 반복해서 의미없는 코드가 돌거나, 너무 많은 Coroutine이 돌아 메모리면에서 OOM 현상이 발생할 수 있고, 관리면에서 쉽게 에러를 유발할 수 있다.
  • 만약 GloablScope.launch만 사용한다면, 이를 관리하기 위해 reference(job)을 모두 가지고 있으면서 join수동으로 관리해야 한다 → 이는 에러를 발생시키기 쉬운 형태이다.
  • 이러한 수동작업을 해결하기 위해, Structured Concurrency(구조화된 동기화)를 사용할수 있다.
  • 이는 GlobalScope에서 launch 하는 대신, Thread를 사용하듯이 우리가 사용하는 Context 범위 내에서 특정한 Coroutine Scope를 만들고, 그 안에서 새로운 CoroutineBuilderCoroutinelaunch하는 것이다.
  • 그렇게 시작하면 생성된 CoroutineScope가 코드블럭의 CoroutineScope에 더해진다.
    = 모든 CoroutineBuilder는 코드 블럭에 “CoroutineScope” Instance를 추가한다.
    1. Coroutine이 다른 CoroutinecoroutinScope내에서 실행되면,
      CoroutineScopre.corotuineContext를 통해, Context를 상속받고,
      부모 Coroutine job의 자식이 된다.
    2. GlobalScope부모 Coroutine영향을 받지 않는다.
  • 따라서 외부 Scope는 내부의 Coroutine들이 종료되기 전까지는 종료되지 않으므로, 우리는 이 scope에서 join 을 명시적으로 부를 필요없이 coroutinelaunch하지 않는 간단한 코드를 만들수 있다.
    부모 coroutine은 항상 children완료를 기다리며, parent는 명시적으로 children의 launchtrack할 필요가 없고 Job.join으로 그들을 기다릴 필요도 없다.
runBlocking{
    val request = launch{
        repeat(3) { i ->
            launch{
                delay((i + 1) * 200L)
                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”)
}
  /* OUTPUT */
  request: I’m done and I don’t explicitly join my children that are still active
  Coroutine 0 id done
  Coroutine 1 id done
  Coroutine 2 id done
  Now processing of the request is complete
  • Coroutine 외부 블럭내부에서 실행되는 Coroutine이 모두 완료되야만 외부 블럭이 완료된다.
fun test2_1() {
    runBlocking {
        val jobs = List(10) {
            launch {
                delay(1000L)
                Log.e(TAG, "aaa")
            }
        }
        // join을 하고 안하고에 따라 End runBlock이 먼저 찍힐지 끝나고 찍힐지가 결정된다
        // jobs.forEach { it.join() }

        Log.e(TAG, "End runBlock ")
    }
    Log.e(TAG, "End function")
}
  1. 위 코드에서는 runBlocking coroutinebuilder로 함수에 coroutine 영역으로 만들었다.
  2. 그리고 그 안에서 새로운 coroutinelaunch한다 (list를 생성하면서 10개를 launch 시킴)
  3. runBlocking내부 coroutine인 list의 열번이 다 수행될때까지 block 되며, launch로 실행한 비동기 동작(자식)이 모두 끝나야 runBlocking{} 블럭이(부모) 종료된다.
  4. End runBlock aaa aaa ... End Function
End runBlock
aaa
aaa
...
End Function
  • 로그는 위와 같이 나오며
  • 주석처리된 join()을 넣으면, 아래와 같은 코드 실행순서가 보장되면서 찍힌다.
aaa
aaa
...
End runBlock
End Function
  • join실행 순서를 보장하기위해 쓰였다.
  • 코드 순서를 보장하기 위해 내부 coroutinejoin시킬수도 있지만,
    Coroutine Scope내부에 만들어 순서를 보장할 수도 있다.
fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking#2")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch#3")
        }
    
        delay(100L)
        println("Task from coroutine scope#1") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over#4") // This line is not printed until nested launch completes
}
Task from coroutine scope#1
Task from runBlocking#2
Task from nested launch#3
Coroutine scope is over#4
  • coroutineScope을 이용하여 내부에 또다른 scope을 만든다.
  • 위에서 모든 coroutine 블럭은 내부(자식) 코루틴이 모두 완료될때까지 대기해야 한다.
  • 이런 이유로 "Coroutine scope is over"은 coroutineScope{...}이 끝날때 까지 기다렸다가 찍힌다
반응형