본문 바로가기

KOTLIN

[KOTLIN IN DEPTH] Kotlin Coroutine Concurrency 2

코루틴 빌더

코루틴을 실행하기 위한 구체적인 영역을 제공하기 위해 여러 가지 함수를 제공하는데 이런 함수를 Coroutine Builder라고 부릅니다. 코루틴 빌더는 CoroutieScope Instance의 확장 함수로 쓰이며 기본적으로 GlobalScope 객체가 있습니다. GlobalScope 를 사용하면 독립적인 코루틴을 만들 수 있고 이 코루틴은 자신만의 작업을 내포할 수 있습니다.

import kotlinx.coroutines.*
import java.lang.System.*

fun main() {
    val time = currentTimeMillis()
   	
    GlobalScope.launch{
        delay(100)
        println("Task 1 finished in ${currentTimeMillis() - time} ms")
    }
    
    GlobalScope.launch{
        delay(100)
        println("Task 2 finished in ${currentTimeMillis() - time} ms")
    }
    
    Thread.sleep(200)
}
// 결과
// Task 1 finished in 214 ms
// Task 2 finished in 217 ms

 

 이 예제를 실행하면 거의 동시에 작업이 끝나는 점에서 알 수 있는 것처럼 실제로는 병렬적으로 실행됐다는 점에 주목해야 합니다. 또한 실행할 때마다 실행 순서가 일정하게 보장되지 않아 상황에 따라 둘 중 어느 한쪽이 더 먼저 출력될 수 있습니다. 이에 코루틴 라이브러리는 필요시 실행 순서를 강제할 수 있습니다. 이에 관한 내용은 동시성 통신에 관련한 절에서 알아본다 하네요!

 

main() 함수 자체는 Thread.sleep() 을 통해 메인 스레드를 잠시 중단시킵니다. 이를 통해 코루틴 스레드가 완료될 수 있도록 충분한 시간을 제공하고 코루틴 스레드는 데몬 스레드로 실행되기 때문에 main() 스레드가 이 스레드 보다 빨리 끝나면 자동으로 실행이 종료됩니다.

 

데몬(Daemon) 스레드
메인 스레드가 종료될 때 같이 종료되는 특별한 종류의 스레드를 말합니다. 데몬 스레드는 주로 백그라운드에서 실행되는 작업에 사용됩니다. 예를 들어, 메인 스레드가 사용자와의 상호작용을 담당하고 있는 동안 백그라운드에서 주기적인 작업을 수행하는 스레드를 데몬 스레드로 만들 수 있습니다. 이런 경우, 메인 스레드가 종료되면 백그라운드 작업도 함께 종료됩니다.

 

suspend 함수에서 delay() 대신 sleep() 같은 스레드를 블록 시키는 함수를 실행할 수도 있지만 이런 식의 코드는 코루틴을 사용하는 목적에 위배되기 때문에 일시 중단 함수인 delay()를 사용해야 합니다. 코루틴은 스레드보다 훨씬 가볍습니다. 특히 코루틴은 유지해야 하는 상태가 더 간단하며 일시 중단되고 재개될 때 완전한 문맥 전환을 사용하지 않아도 되므로 많은 수의 코루틴을 충분히 동시에 실행할 수 있다는 장점이 있습니다.

 

launch() Builder는 결과 반환이 없는 단순 작업의 경우에 적합하고 launch 수행 시 job이 반환됩니다.

with(CoroutineScope(Dispatchers.Main)){
    val job: Job = launch { println(1) }
}

async() Builder는 결과를 반환하며 결과값을 Deffered로 감싸서 반환됩니다. Deferred는 미래에 올 수 있는 값을 담아놓을 수 있는 객체입니다. 

CoroutineScope(Dispatchers.Main).launch{
    val defferedInt: Deferred<Int> = async { 
        print(1)
        1 // 마지막 줄 반환
    }
    val value = defferedInt.await()
    print(value)// 1 출력
}

 

async()는 await() 메서드를 통해 계산 결과에 접근할 수 있습니다. await() 메서드를 호출하면 await()는 계산이 완료되거나 작업이 취소될 때까지 현재 코루틴을 일시 중단시킵니다. 작업이 취소되는 경우에는 await()는 예외를 발생시키면서 실패합니다.

import kotlinx.coroutines.*
import java.lang.System.*

suspend fun main() {
    val msg = GlobalScope.async{
        delay(100)
        "abc"
    }
    val cnt = GlobalScope.async{
        delay(100)
        1+2
    }
    
    delay(200)
    
    val result = msg.await().repeat(cnt.await())
    println(result)
}

// 결과 
// abcabcabc

 

runBlocking() Builder는 디폴트로 현재 스레드에서 실행되는 코루틴을 만들고 코루틴이 완료될 때 까지 현재 스레드의 실행을 Block 시킵니다. 쉽게 설명하면 이름 그대로 Blocking을 시키고 대기 시킵니다. 예를 들면 "runBlocking의 위치가 UI라면 UI를 Blocking 시키고 Coroutines이 끝나길 대기한다" 라는 말이 됩니다. 

 

아래 예제를 보면 runBlocking() 내부의 코루틴은 메인 스레드에서 실행된 반면, launch()로 시작한 코루틴은 공유 풀에서 백그라운드 스레드를 할당받았습니다. 이런 동작 때문에 runBlocking() 은 다른 코루틴 안에서 사용하면 안 되며 Blocking/Non Blocking 호출 사이의 다리 역할을 하기 위해 고안된 빌더 이므로 테스트나 메인 함수에서 최상위 빌더로 사용하는 등의 경우에만 runBlocking을 사용해야 합니다.

import kotlinx.coroutines.*
import java.lang.System.*

suspend fun main() {
    val msg = GlobalScope.launch{
        delay(100)
        println("Background task : ${Thread.currentThread().name}")
    }
    
    runBlocking{
        println("Primary task : ${Thread.currentThread().name}")
        delay(200)
    }
}

// 결과
// Primary task : main @coroutine#2
// Background task : DefaultDispatcher-worker-1 @coroutine#1

 

위 예제만으로 이해가 잘 안되서 아래 예제를 살펴보겠습니다. 이 코드를 실행하면 다음과 같은 상황이 발생합니다.

class MainViewModel : ViewModel() {

    fun load() = runBlocking {
        android.util.Log.w("TEMP", "Thread ${Thread.currentThread()}")
        delay(10000)
    }
}

이런 동작 방식 때문에 runBlocking은 꼭 활용해야 할 시점에 사용해야 하며 주요 내용을 정리해 보겠습니다.

  • runBlocking은 호출한 위치를 이름 그대로 Blocking 시킨다.
  • runBlocking 내부의 응답이 종료되기 전까지 응답을 주지 않는다.
  • runBlocking은 비동기가 아닌 동기로 동작한다.  
  • UI가 Block되면 아무런 동작도 할 수 없다. 그러므로 UI에서 사용하는 runBlocking은 사용하지 않아야 한다.
  • runBlocking이 필요한 케이스를 찾아야 한다. 명확한 IO를 보장하고, 데이터의 동기화가 필요한 경우 활용하자

출처 : https://thdev.tech/kotlin/2020/12/15/kotlin_effective_15/