개요
- 모든 중단 함수에서 일반 함수를 호출하는 것은 문제가 없지만, 일반 함수에서 중단 함수를 호출할 수는 없다.
- 모든 중단 함수는 다른 중단함수에서 호출이 되어야 한다.
- 중단 함수는 어떻게 시작할 수 있을까? 바로 Coroutine Builder를 통해 시작할 수 있다.
launch builder
launch builder 가 동작하는 방식은 개념적으로 thread 함수를 사용해서 thread 를 시작하는 것 과 비슷하다.
독립적으로 코루틴이 실행이 되며 아래 예제와 같이 사용할 수 있다.
fun main() {
GlobalScope.launch {
delay(1000L)
println("World!")
}
GlobalScope.launch {
delay(1000L)
println("World!")
}
GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
// Hello,
// (1 초 후)
// World!
// World!
// World!
위의 예시로 확인 할 수 있는 사항은..
- launch 빌더는 CoroutineScope 의 확장 함수이다.
- delay 함수는 Thread를 black 하지 않지만 Thread.sleep은 Thread를 block 한다. 이유는 이전 장에서 본 것처럼 deley 내부 함수는 코루틴을 통해 다른 Thread에서 Thread.sleep을 사용하기 때문
- launch 함수의 반환값은 Job 이다. 해당 코루틴을 함수에서 반환한다면 외부에서 코루틴을 취소하거나 재개할 수 있다. (join, cancel)
runBlocking builder
위의 launch builder에서는 코루틴이 thread 를 block 하지 않아, 별도의 중단 함수를 추가했다.
runBlocking builder는 별도의 중단함수 없이, thread 를 block 할 수 있다.
runBlocking Builder으로 코루틴이 생성될 경우, 해당 코루틴이 완료될 때 까지 현재 스레드를 block 한다. 따라서 runBlocking 내부에서 delay 함수를 호출 할 경우 Thead.sleep 함수와 비슷하게 작동한다.
fun main(args: Array<String>) = runBlocking {
GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
delay(2000L)
}
runBlokcing이 사용되는 몇가지 사례이다.
- 프로그램이 끝나는 걸 방지하기 위해 thread 를 block 하고 싶을때
- 위의 사유로, Unit Test 를 사용할 때
fun main() = runBlocking {
// ...
}
class MyTest { @Test
fun `a test`() = runBlocking {
// ...
}
}
fun main() = runBlocking {
GlobalScope.launch {
delay(1000L)
println("World!")
}
GlobalScope.launch {
delay(1000L)
println("World!")
} GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
delay(2000L) // 필요함
}
runBlocking builder 은 중요하게 사용되었지만, 현재는 거의 사용되지 않는다.
유닛 테스트에서 가상 시간으로 실행시키는 runTest가 runBlocking 을 대체해서 사용되고 있다.
메인 함수에서는 runBlocking 대신에 suspend 를 붙여 중단 함수로 만들어 사용 할 수 있다.
suspend fun main() {
GlobalScope.launch {
delay(1000L)
println("World!")
}
GlobalScope.launch {
delay(1000L)
println("World!")
} GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
delay(2000L)
}
async builder
async builder는 lauch buidler와 유사하지만, 다른점은 값을 직접 생성한다.
이 값은 lamda 표현식을 통해 반환이 되어야 한다.
await함수는 Deferred<T> 타입의 객체가 반환되며 T 는 생성되는 값의 타입이다.
Deferred에는 작업이 끝나면 값을 반환하는 await 중단 메서드가 있다.
아래 예시를 보면 값이 42가 생성되었고, 타입은 Int 이므로 Deferred<Int> 가 반환된다.
그리고 Deferred<Int>의 await을 통해 42가 반환된다.
fun main = runBlocking {
val resultDeferred: Deferred<Int> = GlobalScope.async {
delay(1000L)
42
}
val result: Int = resultDeferred.await() // (1 초)
println(result) // 42
// 간단하계 작성 가능
println(resultDeferred.await()) // 42
}
async builder 는 launch builder 와 유사하게 호출하는 시점에서 코루틴을 시작한다.
따라서 몇 개의 작업을 한 번에 시작하고, 모든 결과를 기달릴 때 사용된다.
값이 생성되면 Deferred 내부에 저장하기 때문에 바로 사용할 수 있다.
하지만 값이 생성되기 전 await 함수를 호출되는 경우, 값이 준이 준비될 때 까지 기다리게 된다.
fun main = runBlocking {
val res1 = GlobalScope.async {
delay(1000L)
"Text 1"
}
val res2 = GlobalScope.async {
delay(3000L)
"Text 3"
}
val res3 = GlobalScope.async {
delay(2000L)
"Text 2"
}
println(res1.await())
println(res2.await())
println(res3.await())
}
// (1 초)
// Text 1
// (2 초)
// Text 2
// Text 3
async 호출 시점에 코루틴을 시작하므로 3초 이후에 모든 값이 반환된다.
async builder는 launch builder 와 유사하게 동작하지만 값을 반환하는 특징을 가지고 있다.
그 말은 즉슨 lauch builder 은 async builder 로 모두 교체할 수 있지만 용도에 맞게 사용해야 한다.
async builde는 두 가지 데이터를 병렬적으로 가져와 합쳐야 하는 상황일 때 사용할 수 있다.
scope.launch {
val news = async {
newsRepo.getNews().sortedByDescending { it.date }
}
val newsSummary = newsRepo.getNewsSummary()
views.showNews(
newsSummary,
news.await()
)
}
구조화된 동시성
만약 코루틴이 GolbalScope에서 시작되었다면 프로그램은 해당 코루틴을 기디랄 시 없다.
코루틴은 어떠한 thread 도 block 하지 않기 때문이다. 따라서 프로그램이 종료되는 것을 막을 수 없다.
이 이유로 인해 아래의 예제에서 delay 함수를 호출해야 한다.
fun main() = runBlocking {
GlobalScope.launch {
delay(1000L)
println("World!")
}
GlobalScope.launch {
delay(2000L)
println("World!")
}
println("Hello,")
// delay(3000L)
}
// Hello,
(1초뒤)
// World!
(2초뒤)
// World!
Global Scope 를 왜 앞에 붙이는 것일까? 바로 launch 와 async 함수는 CoroutineScope의 확장함수이기 때문이다.
한번 내부 코드를 확인해 보자.
runBlocking
public actual fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
launch
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
async
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
runBlokcing 구현을 보면 마지막 block 파라미터가 CoroutineScope가 리시버 타입인 CoroutineScope 함수 타입인 것을 알 수 있다.
이 는 별도의 CoroutineScope 을 사용하지 않고, 해당 리시버를 통해 launch, async를 호출할 수 있다.
위의 내용을 부모와 자식관계로 풀어내면
runBlocking(부모) - launch(자식) 이고 parent는 child 코루틴을 기다려야 하는 의무가 있기 때문에 child이 모든 작업을 끝마칠때까지 runBlocking은 중단(suspend) 된다.
fun main() = runBlocking {
this.launch { // == launch
delay(1000L)
println("World!")
}
launch { // == this.launch
delay(2000L)
println("World!")
}
println("Hello,")
}
// Hello,
// (1 초)
// World!
// (1 초)
// World!
parent는 children들을 위한 scope를 제공하고,해당 scope 내에서 호출하게 된다.
이를 통홰 구조화된 동시성(Structured Concurrency)이라는 관계가 성립된다.
parent - child 관계의 가장 중요한 특징들 이다.
- child는 parent 의 context 를 상속받는다. (child 은 이를 override 가능합니다.)
- parent 는 모든 children 이 끝마칠 때 까지 기다린다.
- parent coroutine 이 cancel 되면 child coroutine 도 cancel 된다. (반대는 적용되지 않음)
- child coroutine 에서 error 가 발생하면 parent coroutine 또한 error 로 소멸한다. (반대에서도 똑같이 발생함)
위에서 본 것 처럼 coroutine builder와 다르게 runBlocking builder은 CoroutineScope 의 확장함수가 아니다.
runBlocking은 child 가 될 수 없으며, parent coroutine 만이 될 수 있습니다. 따라서 runBlocking은 다른 코루틴과 쓰임새가 많이 다르게 사용된다.
'도서 > 코틀린 코루틴' 카테고리의 다른 글
2장 코루틴 라이브러리 - Cancel (0) | 2024.02.03 |
---|---|
2장 코루틴 라이브러리 - CoroutineContext (1) | 2024.02.03 |
코틀린 코루틴 이해하기 + 코루틴 실전 대입하기 (0) | 2024.01.23 |
1장 코틀린 코루틴 이해하기 - 중단은 어떻게 작동할까? (0) | 2024.01.09 |
1장 코틀린 코루틴 이해하기 - 코틀린 코루틴을 배워야 하는 이유 (0) | 2024.01.08 |