일시중단 가능한 코루틴
코루틴은 기본적으로 일시중단 가능하다. launch로 실행하든 async로 실행하든 내부에 해당 코루틴을 일시중단 해야하는 동작이 있으면 코루틴은 일시 중단된다.
예시로 보는 일시중단
일시 중단 가능하다는 것이 무엇인지 알기 위해 <그림1>을 보자.
위 그림을 코드로 표현하면 다음과 같다. 주석의 숫자와 그림1의 숫자를 일치하도록 하였다.
fun exampleSuspend(){
val job3 = CoroutineScope(Dispatchers.IO).async {
// 2. IO Thread에서 작업3를 수행한다.
(1..10000).sortedByDescending { it }
// 5. 작업3가 완료된다.
}
val job1 = CoroutineScope(Dispatchers.Main).launch {
// 1. Main Thread에서 작업1을 수행한다.
println(1)
// 3. 작업1의 남은 작업을 위해 작업3로부터 결과값이 필요하기 때문에 Main Thread는 작업1을 일시중단한다.
val job3Result = job3.await()
// 6. 작업3로부터 결과를 전달받는다.
// 7. 작업1이 재개된다.
job3Result.forEach {
println(it)
}
}
// 4. Main Thread에서 작업2이 수행되고 완료된다.
val job2 = CoroutineScope(Dispatchers.Main).launch {
println("Job2 수행완료")
}
}
코드를 <그림1>과 함께설명하면
1. Main Thread의 Coroutine1에서 작업1(job1)이 수행되며 Main Thread의 자원을 점유한다.
2. IO Thread의 Coroutine3에서 작업3(job3)가 수행되며 IO Thread의 자원을 점유한다.
3. Coroutine1에서 Coroutine3의 작업3의 결과가 필요한 작업이 나온다. 이때 Coroutine1은 일시중단된다.
4. Coroutine2가 Main Thread를 점유하여 작업2를 수행하고 완료한다.
5. Coroutine3의 작업3이 완료된다.
6. Coroutine1은 작업3의 결과를 전달받는다.
7. Coroutine1이 재개된다.
위의 코드의 결과 로그는 다음과 같다.
I/System.out: 1 // Coroutine1 작업
I/System.out: Job2 수행완료 // Coroutine2 작업
I/System.out: 10000 // Coroutine1 작업 재개
9999
9998
9997
9996
9995
9994
9993
9992
9991
9990
9989
..
job3(작업3)는 IO Thread위에서 수행되는 async 작업이며 결과값을 반환받는 작업이다. 1부터 10000까지를 내림차순으로 정렬해서 반환받는 작업이다보니 job1(작업1)의 println(1)보다는 많은 시간을 소모할 수 밖에 없다.
이 때문에 job1 코루틴은 job3의 결과를 기다려야만 한다. 따라서 중간에 일시 중단 하는 단계가 필요하다. 비동기 작업을 하다보면 이러한 상황을 맞딱뜨리는 경우가 많은데, 이때 코루틴에서는 해당 코루틴 작업을 잠시 일시 중단 가능하도록 한다. 이 때문에 일시 중단 가능한 함수는 코루틴 내부에서 수행되어야 한다.
위의 코드가 약간 복잡하지만, 위의 내용만 제대로 이해해도 코루틴의 일시중단을 제대로 이해할 수 있으니 시간이 조금 걸리더라도 숫자를 하나하나 따라가며 이해하도록 노력해보자.
코루틴 일시 중단은 코루틴 블록 내부에서 수행되어야 한다.
만약 일시 중단을 코루틴 블록(launch 혹은 async) 내부에서 수행하지 않으면 어떻게 될까?
바로 일시 중단 함수로 바꾸라는 오류가 생기게 된다. 코루틴이 일시중단이 되려면 수행되는 위치 또한 코루틴 내부여야 하기 때문이다. 이를 해결하는 방법은 두가지이다. 첫 째는 일시 중단 작업을 코루틴 내부로 옮기는 것이고, 다른 하나는 fun를 suspend fun(일시중단 가능 함수)로 만드는 것이다.
일시 중단 해당 작업을 코루틴 내부로 옮기기
일시 중단 가능한 작업이 코루틴 내부로 옮겨지면 해당 코루틴은 결과값이 올때까지 일시 중단되기 때문에 오류가 사라진다.
fun exampleSuspend() {
val job3 = CoroutineScope(Dispatchers.IO).async {
(1..10000).sortedByDescending { it }
}
CoroutineScope(Dispatchers.Main).launch {
job3.await()
}
}
일시 중단 작업을 수행하는 fun을 suspend fun으로 만들기
fun을 suspend fun으로 바꾸면 함수(fun) 블록에서는 오류가 사라진다. 하지만 그림3에서 볼 수 있듯이 suspend fun를 외부에서 별도 처리 없이 수행하면 오류가 생기는 것을 볼 수 있다.
이유는 suspend fun는 일시중단 가능한 함수를 지칭하는 것이기 때문에, 해당 함수는 무조건 코루틴 내부에서 수행되어야 하기 때문이다. 따라서 <그림3>의 코드는 <그림4>와 같이 코루틴 블록 내에서 수행하도록 바뀌면 오류가 사라진다.
* 추가 : suspend fun은 suspend fun내부에서 수행될 수 있다.
일시중단 함수(suspend fun)는 당연히 일시중단 함수(suspend fun) 내부에서 사용될 수 있다. 당연히 가장 바깥에 있는 일시중단 함수(아래 코드에서는 exampleSuspend2)는 코루틴 내부에서 실행되어야 한다.
suspend fun exampleSuspend2(){
exampleSuspend1()
}
suspend fun exampleSuspend1() {
val job3 = CoroutineScope(Dispatchers.IO).async {
(1..10000).sortedByDescending { it }
}
job3.await()
}
정리
- 코루틴의 일시 중단 기능은 코루틴 내부에서만 수행할 수 있다.
- suspend fun는 일시 중단 가능한 함수로, 해당 함수 내에 일시 중단이 가능한 작업이 있다는 것을 뜻한다.
- 따라서 suspend fun은 코루틴 내부에서 또는 suspend fun 내부에서만 사용할 수 있다.
*위 글에서 suspend fun의 일시 중단 동작을 간단하게 설명하기 위해 CoroutineScope을 재정의 하고 있는데, suspend fun은 부모의 Scope을 coroutineScope { ... } 블록을 통해 접근할 수 있기 때문에, 다음과 같이 쓸 수 있다. 아래와 같이 쓰면 부모 CoroutineScope이 취소될 시 job3도 취소된다.
suspend fun exampleSuspend2() {
exampleSuspend1()
}
suspend fun exampleSuspend1() {
coroutineScope {
val job3 = async(Dispatchers.IO) {
(1..10000).sortedByDescending { it }
}
job3.await()
}
}