쓰레드는 메모리를 굉장히 많이 사용하지만, 코루틴은 굉장히 적은 메모리를 요구한다.
이 스코프는 너무 크기 때문에 잘 사용되지 않는다.
--> 스코프라기 보다는 코루틴 빌더다. launch 와 같은 개념
이 코드의 실행결과는 어떻게 될까?
import kotlinx.coroutines.*
fun main() {
println("Program will now blocking")
runBlocking {
launch {
delay(1000L)
println("Task From runBlocking")
}
GlobalScope.launch {
delay(500L)
println("Task From GlobalScope")
}
coroutineScope {
launch {
delay(1500L)
println("Task From coroutineScope")
}
}
}
println("Program will now continue")
}
메인 쓰레드는 runBlocking 을 만나는순간 runBlocking 의 실행이 끝날때까지 멈추게 된다. 그 이후로는 각각의 스코프가 서로 다른 쓰레드를 사용하기 때문에 delay() 에 주어진 시간이 더 짧은 순서대로 실행되게 된다.
Program will now blocking
Task From GlobalScope
Task From runBlocking
Task From coroutineScope
Program will now continue
스코프가 코루틴을 생성하면서 해당 코루틴에 컨텍스트를 부여한다. 스코프는 코루틴을 제어(시작, 중지)하는 역할을 맡고, 컨텍스트는 코루틴을 실행하는데 필요한 변수와 데이터를 보관하는 역할을 맡는다.
lauch() 함수에 데이터 클래스를 넘겨주면 이를 컨텍스트가 보관한다. 해당 컨텍스트는 코루틴 내부에서 참조할 수 있다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
launch(CoroutineName("coroutine1")) {
println("Task From ${coroutineContext[CoroutineName.Key]}")
}
launch(CoroutineName("coroutine2")) {
println("Task From ${coroutineContext[CoroutineName.Key]}")
}
}
}
컨텍스트를 구성하는 주요 요소 중에는 Dispatcher 와 Job 이 있다. Dispatcher 는 어떤 쓰레드에서 코루틴이 돌아갈지 결정하며, Job 은 코루틴의 라이프 사이클을 통제한다.
코루틴 내부에서 호출할 수 있는 함수이다. 코루틴은 결국 메인스레드와 다른 스레드에서 프로그램을 실행시키는 것이다. 스레드는 각자의 stack 메모리 영역을 갖기 때문에 상호 stack 영역에 접근하기 위해서는 별도의 처리가 필요하지만, 코루틴과 suspend funtion 을 이용하면 단순히 변수 호출만으로 접근이 가능해진다.
아래의 예시에서는 suspend funtion 인 completeMessage 함수에서 메인스레드의 functionCall 변수를 조작하고 있지만,
var functionCall = 0
fun main() {
GlobalScope.launch { completeMessage() }
print("Hello, ")
Thread.sleep(1000L)
println("$functionCall")
}
suspend fun completeMessage() {
delay(500L)
println("World!")
functionCall++ // 코루틴을 실행하는 스레드 안에서 메인 쓰레드에 있는 변수를 변경할 수 있다. 코루틴을 사용하면 간단하게 가능하다.
}
launch 함수는 Job 을 반환한다. 이 Job 을 통해 코루틴 라이프 사이클을 제어할 수 있다.
launch 를 이용해 Job 변수를 만드는것은 launch 의 실행을 담보한다. 아래의 예에서는
invokeOnCompletion , cancel 두 가지 라이프사이클 함수를 사용했다.
invokeOnCompletion 는 해당 코루틴이 cancel 되거나 complete 되어서 실행이 끝났을때 실행된다.
fun main() {
runBlocking {
val job1 = launch {
delay(3000L)
println("Job1 Launched")
}
job1.invokeOnCompletion {
println("Job1 completed")
}
delay(500L)
println("Job1 will be canceled")
job1.cancel()
}
}
// Job1 will be canceled
// Job1 completed
Job 은 계층체계를 갖는데, 이러한 계층체계로 연결된 Job 중에 하나라도 cancel 되면 관련된 모든 Job 들이 멈춘다.
아래의 예시에서는 Job1 아래에 Job2, Job3 를 만들고, delay 를 조작하여 Job2, Job3 가 끝나기 전에 Job1 이 cancel 되도록 하였다. 그 결과 Job3 -> Job2 -> Job1 순서로 끝이 났으며, 계층 체계에 있는 Job 중에 하나라도 cancel 되면 관련된 Job들이 모두 cancel 된다는것을 알 수 있다.
fun main() {
runBlocking {
val job1 = launch {
println("Job1 Launched")
val job2 = launch {
println("Job2 Launched")
delay(3000L)
println("Job2 is finishing")
}
job2.invokeOnCompletion { println("Job2 completed") }
val job3 = launch {
println("Job3 Launched")
delay(3000L)
println("Job3 is finishing")
}
job3.invokeOnCompletion { println("Job3 completed") }
println("Job1 is finishing")
}
job1.invokeOnCompletion {
println("Job1 completed")
}
delay(500L)
println("Job1 will be canceled")
job1.cancel()
}
}
// Job1 Launched
// Job2 Launched
// Job3 Launched
// Job1 will be canceled
// Job2 completed
// Job3 completed
// Job1 completed
job 에 대해서는 join() 함수를 사용할 수 있는데, join 을 쓰게 되면, 해당 job의 하위 job까지 모두 끝날때까지 해당 job을 실행한 코루틴(Invoking coroutine)의 실행을 유보시킨다.
아래의 코드는 Start, Done, delay 1000 ms, delay 1500 ms 순서로 문자가 출력 된다.
fun main() {
runBlocking {
val job1 = launch {
val job2 = launch {
delay(1500L)
println("delay 1500 ms")
}
delay(1000L)
println("delay 1000 ms")
}
println("Start")
println("Done")
}
}
하지만 join 을 써서 코루틴을 유보시킨다면 결과가 달라진다.
job2.join() 을 기점으로 job1(invoking coroutine)의 실행은 job2 의 하위 작업이 모두 끝날때까지 유보되고, job1.join() 을 기점으로 runBlocking(invoking coroutine) 의 실행은 job1의 하위 작업이 모두 끝날때까지 유보된다.
따라서 Start, delay 1500 ms, delay 1000 ms, Done 순서로 문자가 출력이 된다.
fun main() {
runBlocking {
val job1 = launch {
val job2 = launch {
delay(1500L)
println("delay 1500 ms")
}
job2.join() // wait
delay(1000L)
println("delay 1000 ms")
}
println("Start")
job1.join() // wait
println("Done")
}
}
코루틴이 어떤 스레드 풀에도 실행될지는 결정한다. Dispatcher 를 지정하지 않아도 코루틴이 실행되기는 한다. 하지만 코루틴을 사용하는 목적에 따라 Dispatcher 를 지정해주면 성능이 더 좋아진다.
어떤 디스패처는 네트워크 통신에, 혹은 DB 접근에 특화되어 있다.
fun main() {
runBlocking {
launch(Dispatchers.Main) {
println("Main. thread : ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
println("Unconfined1. thread : ${Thread.currentThread().name}")
delay(100L)
println("Unconfined2. thread : ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
println("Default. thread : ${Thread.currentThread().name}")
}
launch(Dispatchers.IO) {
println("IO. thread : ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyThread")) {
println("new. thread : ${Thread.currentThread().name}")
}
}
}
// Main. thread : main
// Unconfined1. thread : main
// Default. thread : DefaultDispatcher-worker-1
// IO. thread : DefaultDispatcher-worker-2
// new. thread : MyThread
// Unconfined2. thread : kotlinx.coroutines.DefaultExecutor
launch 를 이용하는 경우 Exception 이 발생하면 Job 계층체계의 모든 Job 들이 멈추게 된다. 그리고 해당 job 을 join 하는 순간 exception 메시지가 출력된다. 결과를 반환하는 코루틴인 async 를 사용하는 경우 Deferred 된 값을 활용하려고 할때 Exception 이 발생한다.
lauch 로 만들때는 CoroutineExceptionHandler 를 정의해서 넘겨줘야하고, async 를 쓸때는 try ~ catch 를 이용한다.
fun main() {
val handler = CoroutineExceptionHandler {coroutineContext, throwable ->
println("Exception [${throwable.localizedMessage}]")
}
runBlocking {
val job = GlobalScope.launch(handler) {
print("Hello launch~ ")
throw IllegalArgumentException("Example Throw")
println("this will not print")
}
job.join()
val deferred = GlobalScope.async {
print("Hello async~ ")
throw IllegalArgumentException("Example Throw")
}
try {
deferred.await()
} catch (e: Exception) {
println("Exception [${e.localizedMessage}]")
}
}
}
이상의 코드를 봤을때, 코루틴을 만드는 코루틴 빌더를 계속해서 사용하고 있었다.
runBlocking 은 그 자체로 코루틴을 만드는 빌더이다.launch {} 역시 코루틴을 만드는 빌더였다.
그리고 async 가 있다.
async 는 코루틴에서 처리된 결과를 메인쓰레드로 반환해주는 기능을 한다. 당연히 메인 스레드 입장에서는 async 가 반환하는 결과는 미래에 나타나는 결과이기 때문에 Deferred 형태로 반환받아야 한다. 그리고 그러한 결과는 await() 을 통해서 반환 받는다.
fun main() {
var firstValue = 0
var secondValue = 0
runBlocking {
val firstDeferred = async{getFirstValue()}
val secondDeferred = async { getSecondValue() }
firstValue = firstDeferred.await()
secondValue = secondDeferred.await()
}
println("Total Value : ${firstValue + secondValue}")
}
코루틴의 디스패처를 변경하는데 사용되는 함수이다. 이미지 프로세싱 같은 Default 디스패처에서 해야하는 작업을 하고, 해당 이미지를 UI 에 적용해야하는 등의 작업을 처리할 때 사용한다.