하나의 스레드로도 여러 작업을 할 수 있지만 앱이 커짐에 따라 동시 실행으로 여러 코드를 병렬적으로 실행해 리소스를 효율적으로 사용하는 것을 고려해야 한다. 하지만 스레드를 사용한 멀티스레딩은 다음과 같은 문제가 있다.
스레드를 생성하고 전환하고 관리하는 데는 시스템의 리소스가 필요하다. 다른 스레드로 전환하는 컨텍스트 스위칭은 비교적 리소스가 많이드는 작업이다.
실행 중인 앱에는 여러 스레드가 있지만 각 앱에는 메인 스레드가 하나 있고 특히 이 메인 스레드는 앱의 UI를 담당한다. 이 스레드를 메인 스레드 또는 UI 스레드라고도 한다. (UI 스레드와 메인 스레드가 다른 경우도 있다.)
메인 스레드는 UI를 담당하기 때문에, 앱이 자연스럽게 실행되려면 메인 스레드가 잘 동작하는게 중요하다.
프로세서가 여러 스레드의 명령어 집합 간에 전환할 때 스레드가 실행되는 정확한 시간과 스레드가 일시중지되는 시점은 개발자가 제어할 수 없다. 즉 스레드를 직접 사용할 때 순서를 예측할 수 없다.
여러 스레드가 동시에 메모리의 동일한 값에 액세스할 때 경합 상태가 발생할 수 있다.
성능 문제, 경합 상태, 재현하기 어려운 버그는 스레드 직접 사용을 권장하지 않는 이유! 대신 동시 실행 코드 작성에 도움이 되는 코루틴이라는 Kotlin의 기능에 관해 알아보자.
코루틴은 멀티태스킹, 동시성 프로그래밍, 비동기 처리를 지원한다. 코루틴은 스레드를 중단하지 않으면서 비동기적으로 실행할 수 있다.
코루틴의 주요 기능 중 하나는 상태를 저장하여 중단했다가 재개할 수 있다는 것이다.
Job
: 취소 가능한 작업 단위CoroutineScope
: 새 코루틴 생성에 사용되는 launch() 및 async()와 같은 함수는 CoroutineScope를 확장한다.Dispatcher
: 코루틴이 사용할 스레드를 결정한다. 메인 dispatcher는 항상 메인 스레드위에서 코루틴을 실행하지만, Default나 IO 또는 Unconfined 같은 dispatcher는 다른 스레드를 사용한다. GlobalScope
는 앱이 실행되는 동안 내부의 코루틴이 실행되도록 한다. 실제 어플에서 코루틴을 사용할 때는 다른 scope를 사용하기 때문에 권장되는 방식이 아니다.
launch()
는 취소 가능한 Job 객체로 래핑된 enclosed code에서 코루틴을 생성한다. 반환 값이 코루틴의 범위 밖에서 필요하지 않을 때 사용한다.
fun CoroutineScope.launch {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
}
새 코루틴을 시작하고 완료될 때까지 현재 스레드를 차단한다.
runBlocking
은 다음과 같이 사용되는데, 주로 메인 기능과 테스트 사이에서 blocking code와 non-blocking code를 연결하는데 사용된다.
import kotlinx.coroutines.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
val formatter = DateTimeFormatter.ISO_LOCAL_TIME
val time = { formatter.format(LocalDateTime.now()) }
suspend fun getValue(): Double {
println("entering getValue() at ${time()}")
delay(3000)
println("leaving getValue() at ${time()}")
return Math.random()
}
fun main() {
runBlocking {
val num1 = getValue()
val num2 = getValue()
println("result of num1 + num2 is ${num1 + num2}")
}
}
/* 출력
entering getValue() at 09:04:36.568176
leaving getValue() at 09:04:39.574646
entering getValue() at 09:04:39.575694
leaving getValue() at 09:04:42.576174
result of num1 + num2 is 0.8632321377131991
*/
getValue()
는 entering time과 delay(3000) 후 leaving time을 출력한다.
main()
을 다음과 같이 바꾸면, 출력 결과가 달라진다.
fun main() {
runBlocking {
val num1 = async { getValue() }
val num2 = async { getValue() }
println("result of num1 + num2 is ${num1.await() + num2.await()}")
}
}
/* 출력
entering getValue() at 09:06:50.336893
entering getValue() at 09:06:50.340555
leaving getValue() at 09:06:53.340053
leaving getValue() at 09:06:53.343047
result of num1 + num2 is 1.1458268364123785
*/
async()
는 다음과 같이 정의된다.
Fun CoroutineScope.async() {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
}: Deferred<T>
launch()
와 유사한 async()
함수는 Deferred
유형의 값을 반환한다.
Deferred
는
Deferred
의 출력을 기다리도록 하려면 코드 줄에서 await()
를 호출하면 된다.suspend
함수를 호출하려면 그 함수도 suspend
함수여야 한다. getValue()
는 suspend 함수인 delay()
를 호출하기 때문에 suspend이다.
그렇다면 getValue()
를 호출하는 main()
은 왜 suspend가 아닐까? 사실 getValue()
는 runBlocking()
으로 전달된 suspend 함수에서 호출되는 것이다. 즉 사실상 main()
에서 호출하는 것도, runBlocking()
에서 호출하는 것도 아니기 때문에 main()
은 suspend가 아니다.
// Thread
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
Thread {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
Thread.sleep(50)
}
}.start()
}
}
/*
Thread[Thread-1,5,main] - Doing Task 1 Thread[Thread-0,5,main] - Doing Task 1
Thread[Thread-2,5,main] - Doing Task 1 Thread[Thread-1,5,main] - Doing Task 2
Thread[Thread-0,5,main] - Doing Task 2 Thread[Thread-2,5,main] - Doing Task 2
Thread[Thread-1,5,main] - Ending Thread[Thread-0,5,main] - Ending
Thread[Thread-2,5,main] - Ending Thread[Thread-0,5,main] has started
Thread[Thread-2,5,main] has started
Thread[Thread-1,5,main] has started
Thread[Thread-1,5,main] - Starting
*/
// Coroutine
import kotlinx.coroutines.*
fun main() {
val states = arrayOf("Starting", "Doing Task 1", "Doing Task 2", "Ending")
repeat(3) {
GlobalScope.launch {
println("${Thread.currentThread()} has started")
for (i in states) {
println("${Thread.currentThread()} - $i")
delay(5000)
}
}
}
}
/*
Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main] has started
Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main] - Starting
Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] has started
Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main] - Starting
Thread[DefaultDispatcher-worker-2 @coroutine#3,5,main] has started
Thread[DefaultDispatcher-worker-2 @coroutine#3,5,main] - Starting
*/