엄코딩의 개발 일지

Coroutin GlobalScope

 

 GlobalScope.launch { // launch new coroutine in background and continue
            delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
            Log.e("coroutine","World!") // print after delay
        }
        Log.e("coroutine","Hello,")
        Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
Hello,
World!

 

본질적으로 코루틴은 light-weight threads 입니다.  코루틴은 CoroutineScope의 context에서 시작된 Coroutine builder로 시작됩니다.

여기서 우리는 GlobalScope에서 새로운 코루틴을 시작합니다. 즉, 새로운 코루틴의 생명주기는 Application의 생명주기로 제한됩니다.

 

GlobalScope.launch { ... } with thread { ... } and delay(...) with Thread.sleep(...). Try it.

 

GlobalScope.launch 를 thread로 바꾸어 보면 아래와 같은 에러 문구를 확인할 수 있습니다.

 

Suspend function 'delay' should be called only from a coroutine or another suspend function

 

delay()는 스레드를 차단하지 않으며, 코루틴에서만 사용할 수 있습니다.

 


runBlocking { ... }

 

아래 코드는 runBlocking 코루틴 빌더를 사용하여 blocking에 대해 명시적으로 알아보겠습니다.

fun main() { 
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

 

처음 위 코드를 접했을 때 무슨말인지 몰랐습니다. 그래서 로그를 추가하여 순서를 파악해 보았습니다.

 

GlobalScope.launch { // launch new coroutine in background and continue
            delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
            Log.e("sequence","4") // print after delay
        }
        
        Log.e("sequence","1")

        runBlocking {     // but this expression blocks the main thread
            delay(4000L)  // ... while we delay for 2 seconds to keep JVM alive
            Log.e("sequence","3")

        }
        
        Log.e("sequence","2")

 

어떻게 출력될까요?

 

출력 순서는 1 4 3 2 입니다. 즉, GlobalScople.~에서 delay는 메인스레드를 막지 않고, runBlocking은 메인스레드를 막고 있습니다.

 

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

 

위 코드를 통해서 runBlocking을 사용하여 main 함수를 더 관용적인 방법으로 다시 작성할 수 있습니다.

 

 runBlocking<Unit> { ... }

 

위 코드는 최상위 메인 코루틴을 시작하기 위해 사용되는 어댑터로서의 역할을 하게됩니다.

Kotlin의 Unit 즉, 올바른 형식의 main 함수가 반환되어야하기 떄문에 반환 형식을 명시적으로 Unit으로 지정합니다.

 

join()

다른 코루틴이 작동하는 동안 delay하는 것은 좋지 않은 접근 방법입니다.

백그라운드 작업이 완료될때까지 join()을 사용하여 명시적으로 기다려야합니다.

 

fun main() = runBlocking {
    val job = GlobalScope.launch { // launch new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes    
}

 


 

구조화된 동시성

 

코루틴의 실용적인 사용을 위해 여전히 요구되는 사항이 있습니다. 우리가 GlobalScope.launch 사용할 때 최상위 레벨의 코루틴을 생성합니다. 물론 light-weigh 이지만 실행되는 동안 일부 메모리 자원을 여전히 소모합니다. 새로 시작된 코루틴에 대한 참조를 유지하는 것을 잊어버리면 여전히 실행됩니다. 코루틴의 코드가 멈추는 경우 ( 예를 들어 너무 오랜 시간동안 delay되는 경우 ), 너무 많은 코루틴을 실행하여 메모리가 부족하면 어떻게 될까요?

즉, 실행된 모든 코루틴에 대한 참조를 수동으로 유지하고 이들을 join() 해야 하는 것은 오류가 발생하기 쉽습니다.

 

해결방법은 GlobalScope에서 coroutines를 시작하는 대신 일반적으로 Thread( thread는 항상 전역 )와 마찬가지로, 수행중인 작업의 특정 범위에서 동시 루틴을 시작할 수 있습니다.

 

runBlocking를 포함한 모든 코루틴 빌더는 CoroutineScope 인스턴스를 추가합니다. 외부 코루틴에서는 범위 내에서 시작된 모든 코루틴이 완료 될 때까지 코루틴을 완료하지 않기 때문에 명시적으로 처리하지 않고도 ( join() ) 이 범위에서 코루틴을 시작할 수 있습니다.

 

아래 코드는 위의 코드를 join() 사용하지 않고 더 간단하게 작성한 코드입니다.

 

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

 

Scope builder

다른 빌더가 제공하는 코루틴 Scope 외에도 coroutineScope 빌더를 사용하여 자신의 범위를 선언할 수 있습니다.

새로운 코루틴 범위를 작성하고 시작한 모든 자식이 완료될 때까지 완료되지 않습니다.

runBlocking 와 coroutineScope의 주요 차이점은 runBlocking는 모든 자식이 완료될 때까지 기다리는 동안 현재 스레드를 차단하지 않고,

coroutineScope는 모든 자식이 완료될 때 까지 기다리지 않고 현재 스레드를 차단합니다.

 

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

출력 결과

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over

 

Extract function refactoring

launch { ... } 코드 블록을 별도의 함수로 추출해 봅시다. 이 코드에서 "Extract function" 리펙토링을 실행하면 suspend 함수로 작성할 수 있습니다. 이것이 첫번쨰 suspend 기능입니다. suspending functions는 코루틴에서 사용할 수 있지만 추가적으로 동시에 다른 suspend 함수 ( delay 등 )을 사용할 수 있습니다.

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}