Kotlin Coroutines - Coroutines basics
모든 프로그래밍 언어익힐 때 찍어보는 문장으로 “Hello, World!”가 있다.
마찬가지로 먼저 “Hello, World” 를 코루틴으로 찍어보도록 하자.
1. runBlocking
코루틴을 만드는 가장 간단한 방법으로 runBlocking
함수가 있다.
이름에 blocking이 들어가있는 것에서 알 수 있듯 runBlocking
함수는 코루틴을 만들고, 해당 코드 블록의 수행이 끝날 때까지
다음 코드를 수행하지 않도록 막아준다.
아래 예제를 먼저 보자.
import kotlinx.coroutines.* fun main() = runBlocking { println(Thread.currentThread().name) println("Hello, World!") }
출력 결과
1 | main @coroutine#1 |
위의 코드에서 Thread.currentThread().name
는 현재 스레드아 무엇인지 출력해주는 역할을 한다.
코루틴이 스레드를 사용하는 것인만큼 앞으로도 자주 쓰게 될 것이다.
2. 코루틴 빌더의 수신 객체
runBlocking
블록 안에서 this
를 출력하면 무엇이 찍힐까?
아래 예제를 실행해보자.
import kotlinx.coroutines.* fun main() = runBlocking { println(this) println(Thread.currentThread().name) println("Hello, World!") }
출력 결과
1 | "coroutine#1":BlockingCoroutine{Active}@36d64342 |
위와 같이 출력이 된다.
여기서 this
가 출력된 "coroutine#1":BlockingCoroutine{Active}@36d64342
을 통해 코루틴이 수신 객체인 것을 추론할 수 있다.
Active
는 출력 당시 해당 코루틴이 현재 활성화 상태임을 뜻한다.
참고 코틀린에서 수신 객체란 확장 함수가 호출되는 대상이 되는 값 또는 객체 이다. Extension Lambda라고 부르기도 한다.
여기서 BlockingCoroutine
은 CoroutineScope
인터페이스의 구현체이다.
CoroutineScope
인터페이스는 coroutineContext
를 가지고 모든 코루틴에 위치하므로,
코틀린 코루틴을 사용하는 모든 곳에 CoroutineScope
가 있다고 볼 수 있다.
아래의 코드를 참조하자.
1 | // CoroutineScope.kt |
3. CoroutineContext
CoroutineScope
는 코루틴을 제대로 처리하기 위한 정보인 CoroutineContext
를 가지고 있다.
상술했듯 runBlocking
은 수신 객체를 가지고 있기때문에, 블록 내에서 코루틴 객체 안에 있는 것처럼 코드를 작성할 수 있고
CoroutineContext
속성을 출력할 수도 있다.
import kotlinx.coroutines.* fun main() = runBlocking { println(coroutineContext) println(Thread.currentThread().name) println("Hello, World!") }
출력 결과
1 | [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@340f438e, BlockingEventLoop@30c7da1e] |
출력 결과 [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@340f438e, BlockingEventLoop@30c7da1e]
가 출력되었다.
CoroutineId(1)
는 코루틴의 id값를
"coroutine#1":BlockingCoroutine{Active}@340f438e
는 현재 코루틴의 정보를
BlockingEventLoop@30c7da1e
는 현재 코루틴이 호출된 스레드를 출력해준다.
참고 Android는
Thread.currentThread()
로 코루틴 id를 출력할 수 없다.CoroutineName
을 지정해주어야 한다.
참고
runBlocking
에 지정되는 Dispatcher의 기본값은Dispater.Main
이 아니다.
4. launch
이번엔 runBlocking
이 아닌 launch
코루틴 빌더를 사용해보자.
launch
는 새로운 코루틴을 생성하므로, runBlocking
과 달리 코루틴 블록 이후의 코드도 같이 실행한다.
단 launch
는 runBlocking
으로 코루틴을 만든 후, 새로운 코루틴을 생성하므로 아래 코드와 같이 이중 구조를 가지게 된다.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") println("World!") } println("runBlocking: ${Thread.currentThread().name}") println("Hello, ") }
출력 결과
1 | runBlocking: main @coroutine#1 |
출력 결과를 보면 runBlocking
이 먼저 수행되고 이후 launch
가 실행되었음을 알 수 있다.
현재 예제 코드상으로 runBlocking
과 launch
둘다 메인 스레드를 사용하고 있기때문에 runBlocking
블록의 코드들이 메인 스레드를 다 사용할 때까지 launch
블록의 코드들이 모두 기다리게 된다.
runBlocking
블록 또한 launch
블록의 코드가 모두 수행될때까지 기다리게 된다.
5. delay
일반적으로 Java에서 스레드를 사용하는 경우, 지연을 위해 Thread.sleep()
을 사용한다.
코루틴은 지연을 위해 delay
를 사용한다.
delay
를 사용하면 해당 스레드를 해제하고 잠시 지연되는 형태로, 지연되는 동안 다른 코루틴이 스레드를 사용할 수 있게 해준다.
아래 예제를 보자.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") println("World!") } println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("Hello, ") }
출력 결과
1 | runBlocking: main @coroutine#1 |
이번에도 runBlocking
블록이 먼저 수행되지만, delay
를 만나며 launch
블록이 수행된 후 delay
이후에 println("Hello, ")
가 호출되었음을 확인할 수 있다.
이때 스레드를 해제하고 다른 코루틴에 양보하는 것은 출력 결과에서 메인 스레드를 사용하는 것으로 확인할 수 있다.
만약 delay
없이 수행하면 어떻게 될까?
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") println("World!") } println("runBlocking: ${Thread.currentThread().name}") // delay(500L) println("Hello, ") }
출력 결과
1 | runBlocking: main @coroutine#1 |
runBlocking
이 끝나고 launch
가 수행되므로 예상대로 위의 출력 결과를 얻을 수 있다.
이번엔 launch
블록에도 delay
를 추가해보자.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") delay(100L) println("World!") } println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("Hello, ") }
출력 결과
1 | runBlocking: main @coroutine#1 |
첫 번째 delay(500L)
을 만나 메인스레드에 launch
코루틴이 가져간다.
이후 delay(100L)
을 만나게 되지만 0.5초보다 지연이 적으므로, 수행이 완료된 후 runBlocking
의 나머지 코드를 수행하게 된다.
6. Thread.Sleep()
in Coroutines
코루틴 블록 안에서 delay
가 아닌 Thread.Sleep()
을 호출하면 동일한 결과가 나올까?
답은 “아니다” 이다.
delay
는 스레드를 양보하지만, Thread.Sleep()
은 스레드를 점유한 채로 지연하기 때문이다.
아래 예제를 통해 비교해보자.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") println("World!") } println("runBlocking: ${Thread.currentThread().name}") Thread.sleep(500) println("Hello") }
출력 결과
1 | runBlocking: main @coroutine#1 |
Thread.Sleep()
은 스레드를 점유하기때문에 runBlocking
의 코드 수행이 먼저 되고 launch
블록이 수행되었음을 알 수 있다.
다시 delay
로 교체해보자.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch: ${Thread.currentThread().name}") println("World!") } println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("Hello") }
출력 결과
1 | runBlocking: main @coroutine#1 |
이땐 launch
블록이 먼저 수행되고 runBlocking
블록이 마저 수행되었을 알 수 있다.
7. Multiple launch
이제 launch
를 이용해 여러 개의 코루틴을 생성하고 각각 다른 delay
를 부여해보자.
import kotlinx.coroutines.* fun main() = runBlocking { launch { println("launch1: ${Thread.currentThread().name}") delay(1000L) println("3!") } launch { println("launch2: ${Thread.currentThread().name}") println("1!") } println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("2!") }
출력 결과
1 | runBlocking: main @coroutine#1 |
먼저 runBlocking
블록이 수행되다가 delay(500L)
을 만나 지연되고, 첫 번째 launch
블록을 수행하게 된다.
이후 delay(1000L)
을 만나 첫 번째 launch
블록도 지연되고, 두 번째 launch
블록의 1!
을 출력한다.
이후 delay(500l)
의 지연이 끝나 2!
를, 마지막으로 delay(1000L)
의 지연도 끝나 3!
을 호출하면서 마무리된다.
또한 모두 메인 스레드에서 수행되었음을 확인할 수 있다.
이렇게 delay
등으로 인해 지연되는 포인트 등을 중단점(Suspension point) 라고 한다.
8. Hierarchical structure
본 포스팅의 예제들은 runBlocking
안에 launch
블록을 작성되었다.
코루틴은 계층적 구조를 가지고 있어 runBlocking
블록은 내부의 모든 launch
블록들이 종료될 때까지 종료되지않는다.
즉, 상위 코루틴은 하위 코루틴을 끝까지 기다리는 구조이다.
import kotlinx.coroutines.* fun main() { runBlocking { launch { println("launch1: ${Thread.currentThread().name}") delay(1000L) println("3!") } launch { println("launch2: ${Thread.currentThread().name}") println("1!") } println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("2!") } print("4!") }
출력 결과
1 | runBlocking: main @coroutine#1 |
runBlocking
의 지연보다 더 큰 지연 시간을 첫 번째 launch
블록이 가지고 있지만, 모든 launch
가 종료될때까지 기다렸다가 4!
가 제일 마지막에 출력됨을 알 수 있다.
9. suspend
코루틴이 계층적 구조를 제공한다고 해서, 모든 처리 로직을 runBlocking
블록 내에서 작성한다면 가독성이 떨어지게 된다.
코루틴의 내부 동작 혹은 일부 동작을 함수로 분리하고 싶은 경우 suspend
키워드를 사용할 수 있다.
suspend
는 중단 가능하단 뜻으로, 코루틴 내에서 호출하거나 다른 코루틴을 호출할 수 있게 해준다.
import kotlinx.coroutines.* suspend fun doThree() { println("launch1: ${Thread.currentThread().name}") delay(1000L) println("3!") } suspend fun doOne() { println("launch1: ${Thread.currentThread().name}") println("1!") } suspend fun doTwo() { println("runBlocking: ${Thread.currentThread().name}") delay(500L) println("2!") } fun main() = runBlocking { launch { doThree() } launch { doOne() } doTwo() }
출력 결과
1 | runBlocking: main @coroutine#1 |
단 doOne()
함수의 경우 코루틴 내에서만 호출 가능한 delay
함수를 쓰지않았기때문에 suspend
를 붙이지 않아도 호출할 수 있다.
반대로 아래와 같이 delay
함수에 suspend
가 붙어있기에 suspend
혹은 코루틴 내에서만 호출할 수 있는 것이다.
1 | public suspend fun delay(time: Long) { |
외부로 분리할 수 있음을 알고나서 한 가지 더 확인해봐야할 것이 있다.
바로 runBlocking
블럭에 <>
를 이용해 반환 타입을 아래와 같이 지정할 수 있다.
기본값은 Unit
이며, 반환값이 없다는 뜻이기에 상술한 예제들에서 별도의 타입을 명시하지않았다.
1 | fun main() = runBlocking<Unit> { |
만약 Unit
대신 Int
로 변경하고 반환값을 지정하는 경우 오류가 발생하는데, 코틀린의 main
함수는 무조건 Unit
을 반환해야하기 때문이다.
10. Use builder within scope
suspend
키워드를 가진 함수를 main
에서 호출해보도록 하자.
import kotlinx.coroutines.* suspend fun doSomething() { launch { println("launch: ${Thread.currentThread().name}") delay(100L) } } fun main() = runBlocking { doSomething() }
출력 결과
1 | Unresolved reference: launch |
위의 예제는 출력 결과와 같은 오류 메시지를 출력하며 동작하지않는다.
하나씩 살펴보자.
1 | Unresolved reference: launch |
suspend
키워드는 “중단점 역할을 할 수 있다”를 의미하는 것이기 코루틴 스코프를 생성해주는 키워드가 아니다.
launch
를 통해 새로운 코루틴을 만들기 위해 코루틴 안에서 실행되어야 함을 명심하자.
1 | Suspension functions can be called only within coroutine body |
이 오류는 delay
로 인해 발생하였다. delay
또한 suspend
키워드를 가지고 있기에 코루틴안에서 호출해야 한다.
이를 해결하려면 어떻게 해야할까? 답은 간단하다.
doSomething()
함수를 코루틴으로 만들어주면 되는 것이다.
이때 coroutineScope
함수를 사용해 코루틴으로 만들어줄 수 있다.
import kotlinx.coroutines.* suspend fun doSomething() = coroutineScope { launch { println("launch: ${Thread.currentThread().name}") delay(100L) } println("coroutineScope") println("coroutineScope: ${Thread.currentThread().name}") } fun main() = runBlocking { doSomething() }
출력 결과
1 | coroutineScope |
coroutineScope
를 통해 코루틴을 부여하였으니, 내부에서 launch
를 호출할때마다 새로운 코루틴을 만들게 되었다.
단 coroutineScope
으로 처리하게 되면 반환값이 필요해지므로 println
을 추가해 Unit
타입으로 만들어주었다.
언뜻 보기엔 runBlocking
과 별반 차이가 없어보이지만 Blocking이 없다는 것에 주목하자.
coroutineScope
는 현재 스레드를 멈추게 하지않고 호출한 쪽이 suspend
되고, 지연 혹은 수행이 완료되면 다시 이어서 수행하게 된다.