Coroutine?
-
루틴의 일종으로, 협동 루틴으로 이해하면 쉽다.
-
코루틴의 "Co"는 with, together를 뜻한다.
-
코루틴은 자신의 실행이 마지막으로 중단된 지점 다음의 포인트부터 동작을 재개한다. 즉, 순차 실행이 가능하다.
-
(tmi) 코루틴 최초의 출판물은 1963년에 등장했다. -
러닝 커브는 다음 그림 참고(출처 바로가기)

※ IntelliJ나 Android Studio가 설치되어 있지 않다면 Kotlin 공식 사이트에서 지원하는 온라인 IDE(Kotlin Playground)를 사용하여 아래 코드를 실행할 수 있다.
Your First Coroutine
아래 코드를 실행하면,
1 | fun main() { |
Would!Hello, 가 출력될까?
예상했겠지만, 당연히 답이 아니다. main() 이 실행되고, GlobalScope.launch 에서 1초의 딜레이가 걸린다. 이때 딜레이는 GlobalScope.launch 에만 영향을 주기에 main() 의 코드는 그대로 실행되며, 우리 모두가 아는 Hello,World! 의 출력을 확인할 수 있다.
정확히는 Hello, 출력 후 1초의 딜레이, 그리고 World! 출력 후 1초 딜레이 이후 해당 프로그램이 종료될 것이다.
정리
- 코루틴은 가벼운 스레드의 일종으로 볼 수 있다.
- coroutine builder인
launch를 이용하여 코루틴을 생성할 수 있다. launch를 사용하기 위해서는 CoroutineScope인 GlobalScope를 사용해야 한다.GlobalScope.launch {}는therad {}로 변환될 수 있다.
Bridging blocking and non-blocking worlds
Thread.sleep(2000L) 로 끝난 코드를 runBlocking 으로 바꿔보는 예제이다. 이전에, delay 와 sleep 의 차이를 짚고 넘어가자.
delay- 일시 중단되는(suspend) 함수
sleep- thread를 blocking하는 함수
즉, 이번 예제의 목표는 main() 함수에서 suspend 함수 사용법을 배우는 것이다. 아래 코드를 참고하자.
1 | fun main() { |
runBlocking 을 사용하면 blocking하는 코루틴을 생성할 수 있다.
정리
- 코루틴 빌더인
runBlocking을 사용하면 내부 코루틴이 완료될 때까지 메인 스레드가 blocking되어 프로그램이 중단되지 않는다.
코드를 조금 더 깔끔하게 작성할 수는 없을까?
1 | fun main() = runBlocking { |
main() 함수를 runBlocking 으로 감싸서 전체 flow가 완료되기 전까지 종료되지 않는 코드로 변신!
Waiting for a job
delay 없이 비동기 작업을 깔끔하기 처리하기 위해서는 job 객체에 join() 하는 방법이 있다.
1 | fun main() = runBlocking { |
위의 코드를 보고 ‘그럼 runBlocking은 불필요한 것 아닌가?’ 하는 생각이 들어 코드를 지우고 실행하니 정상적으로 동작하지 않았다. join()[1] 은 suspend function으로, 코루틴 내부나 다른 suspend function에서만 실행할 수 있다! 잊지 말 것!
여기서 잠깐! 그럼 main 에 job 을 100개 가지는 코루틴은 job 마다 join 을 전부 해 줘야 하는 걸까? 호엥? 너무 비효율적이잖아?
Structured concurrency
그래서 코루틴에서는 이런 비효율을 개선하기 위해 runBlocking 과 launch 의 관계성을 확립하는 구조를 제안한다. 코드를 보자.
1 | fun main() = runBlocking { |
GlobalScope 에서 launch 하지 말고 runBlocking 에서 launch 로 코루틴을 생성하는 방법이다. 이렇게 생성할 경우 코루틴이 몇 개가 생성되든 main 함수는 해당 job 들이 모두 끝나기 전까지 프로그램을 종료하지 않는다.
Scope builder
위와 비슷한 방법으로 coroutineScope 를 이용해서 범위를 선언할 수도 있다. 코루틴 범위를 만들고 그 안에서 만들어진 모든 자식 코루틴이 완료되기 전까지 끝나지 않는 방식이다. 언뜻 보면 structured concurrency와 비슷해 보이지만 차이점이 있다.
runBlocking- 대기하기 위해 현재 스레드를 차단한다.
coroutineScope- 다른 용도로 사용하기 위해 기본 스레드 상태를 해제한다.
아래 코드 실행 순서를 눈여겨보자.
1 | fun main() = runBlocking { |
편의를 위해 문장에 번호를 추가했다. 어떤 순서로 실행될까? 이 코드를 처음 봤을 때 4-3-1-2 순서[2]로 실행된다고 생각했지만 정답이 아니었다. 위의 코드는 3-1-2-4 순서로 실행된다. 즉, coroutineScope 가 완료되지 않은 상태에서도 3. Task from coroutine scope 바로 뒤에 1. Task from runBlocking 이 실행됨을 알 수 있다.
정리
delay대신job.join()을 이용하면 비동기 작업이 깔끔해진다.join()은 코루틴 내부나 다른 suspend function에서만 실행할 수 있다.join()오조 오억 개 쓰기 싫으면structured concurrency사용해라.runBlocking은 대기하기 위해 현재 스레드를 차단(coroutineScope의 플로우가 모두 실행되기 전까지 4번 문장을 출력하지 않음)한다.coroutineScope는 다른 스레드를 실행하기 위해 현재 스레드를 suspend한다. 즉, 코루틴을 생성한 최상위 함수가 동일한 스레드에서 계속 실행될 수 있다.
Extract function refactoring
코루틴 내부에서 호출할 함수에 delay 등의 suspend 함수를 사용하고 싶다면 다음 코드를 주목하자.
1 | fun main() = runBlocking { |
정리
- 일반 함수를 suspend 함수로 바꾸고 싶다면
suspend를 붙이면 된다.(심플)
Global coroutines are like daemon threads
코루틴이 계속 실행되고 있다고 해서 스레드가 실행 중이라고 볼 수 없다. 프로세스가 살아 있을 때에만 동작할 수 있다.
1 | fun main() = runBlocking { |
위의 코드를 실행시켜 보면 1.3초 이후에는 main 함수가 종료되고, 이와 동시에 repeat() 도 멈추는 것을 확인할 수 있다. 즉, 메인 프로세스가 종료될 경우 그 안에 생성된 코루틴의 동작 또한 모두 중지된다.