Coroutine
코루틴은 기존에 비동기 처리 시의 Callback 구현, cancel 작업, Resource 관리 등을 해야하는 불편을 줄이고 비동기 처리를 간단하게 할 수 있도록 해주며, MainThread가 bockling 되는 상태를 관리할 수 있도록 도움을 준다고 한다.
보통 네트워크를 call하고 ui를 업데이트 시키는 작업을 할 때 MainThread에서 비동기 처리가 허용되지 않아 Exception: NetworkOnMainThreadException 예외가 발생하고, 이 것을 위해 thread에 작업을 시키면, ui를 업데이트 시키는 작업은 Ui Thread에서 작업해야 하므로 또 Exception: CalledFromWrongThreadException 예외가 발생한다. 그래서 thread를 이용해 네트워크를 call하고 MainThread로 Callback을 주어 ui를 업데이트하는 등의 작업을 했다. 하지만 이 또한 문제인 것이 Call한 객체들을 사용 후 메모리를 위한 작업을 해줘야하고(clear, disposition), 이러한 작업을 하려니 cancel() 코드가 너무 많이 들어가거나, Callback 지옥에 빠지는 등의 문제가 있다.
위에서 말한 불편함을 줄이기 코루틴이 등장했다.
● 코루틴은 루틴의 일종
● 협력형 멀티 태스킹
● 동시성 프로그래밍 지원
● 편리한 비동기 처리
동기 : 말 그대로 동시에 작업이 이루어진다는 의미 (요청 -> 결과가 동시에 발생)
비동기 : 동시에 작업이 이루어지지 않는다는 의미 (요청 -> ... -> 결과)
즉 결과가 주어지는데 오랜 시간이 걸리는 작업에 대해서 비동기 방식으로 처리하는게 좋다, 결과가 주어지는 시간까지 다른 작업을 할 수 있으므로 자원의 효율면에서도 좋다.
코루틴은 cooperative tasks(협력작업), exceptions(예외), event loops(이벤트 루프), iterators(반복자), infinite lists(무한 목록), pipes(파이프)와 같은 프로그램 구성 요소를 구현하는데 적합하다고 한다.
0. 핵심 키워드
CoroutineScope : 코루틴 블록을 제어할 수 있는 범위
CoroutineContext : 코루틴을 어떻게 처리할 지 대한 집합으로 Job과 dispatcher가 있다.
Dispatcher : CoroutineContext을 상속받아 어떤 스레드를 이용할 지 정의
Dispatchers.Default - 주 스레드에서 작업하기엔 Cpu 사용량이 높은 작업에 적합
Dispatchers.IO - 네트워크, 디스크 사용 시 적합하며, 이에 대한 작업에 최적화되어 있다.( IO 스레드)
Dispatchers.Main - UI 스레드
launch, async는 CoroutineScope의 확장함수로, 넘겨받은 Coroutine 블럭에 대해 Coroutine을 만들고 실행해주는 Coroutine Builder이고, 각각 Job, Deferred 객체를 반환한다.
runBlocking : runBlocking 또한 Coroutine 블럭을 만들지만 내부 작업이 종료될 때까지 일시 중지된다.
1. 협력형 멀티 태스킹
코루틴의 "Co"는 together나 with를, Routine은 하나의 함수를 뜻한다고 생각해보자, 즉 협력을 하는 함수!!
Routine이란 main과 sub Routine이 존재한다.
fun main() { // Main Routine
print(subRoutine())
}
fun subRoutine() : String { // Sub Routine
return "hello"
}
위의 코드를 보면 알 수 있듯이 Main 함수에서 Sub 함수를 호출하고 있다. 이러한 Routine에선 subRoutine 함수를 진입하고 탈출하고 탈출하는 시점이 정해져있다. 여기서 Coroutine과 다른 점이 존재한다.
Coroutine에선 진입점과 탈출점이 여러 개라는 것이다. 그럼 어떻게해야 Coroutine을 사용할 수 있을까?
Coroutine을 사용하려면 Coroutine 블럭이 필요하며, 이 블럭으로 하나의 Coroutine이 되게된다, 즉 중단 -> 재개를 할 수 있는 자격이 주어진다고 보면 되는 것이다.
Coroutine 빌더를 통해 블럭을 묶음으로 제어할 수 있게 해준다.
자격은 주었으나, 아직 중단 -> 재개를 할 수 없다. 이를 가능하게 하려면 suspend 키워드가 필요하다. suspend 키워드로 선언된 함수에 접근하면 위에서 말했듯이 Coroutine 블럭을 중단 -> 재개 할 수 있게되면다.
suspend fun main() {
coroutineScope {
launch {
subRoutine()
subRoutine1()
subRoutine2()
}
}
}
suspend fun subRoutine() {
delay(2000)
println("hello")
}
suspend fun subRoutine1() {
delay(2000)
println("...")
}
suspend fun subRoutine2() {
delay(2000)
println("bye")
}
실행결과
hello (2초 뒤 출력)
... (2초 뒤 출력)
bye (2초 뒤 출력)
이제 기본적인 예제를 하면서 알아보도록 합시다.
GlobalScope.launch {
delay(1000L)
println("two")
}
println("one, ")
Thread.sleep(2000L) // 메인 스레드 Blocking : main()이 끝나지않게 하는 것
실행결과 :
one,
two
launch : 코루틴 빌더 역할을 한다고 보면 된다. 내부적으로 코루틴을 만들어서 반환해준다.
GlobalScope : GlobalScope은 이미 만들어진 객체인데, GlobalScope는 코루틴 중 프로그램의 전역 스코프 역할한다.
위의 코드에서 코루틴과 스레드를 혼용하고 있으니 Thread.sleep(2000L)를 코루틴으로 변경해보았다.
GlobalScope.launch {
delay(1000L)
println("two")
}
println("one, ")
runBlocking {
delay(2000L)
}
실행결과 :
one,
two
실행 결과는 같지만 runBlocking이라는 코루틴을 반환하는 빌더를 이용해 delay()를 사용할 수 있도록 하였다.
runBlocking은 runBlocking을 사용하는 위치가 UI라면 UI를 Blocking(멈춤) 시키고 runBlocking 내부의 작업들이 처리한 후 Blocking을 해제한다.
하지만 만약 GlobalScope에 있는 delay()를 3000L로 하게되면 two는 출력되지 않고 프로그램이 종료된다.
이러한 부분 때문에 delay() 대신 job()을 사용할 수 있다.
val job = GlobalScope.launch {
delay(3000L)
println("two")
}
println("one, ")
job.join()
실행결과 :
one,
two
launch가 실행되게되면 Job이 반환된다. 위의 코드에서 볼 수 있듯이 반환된 Job을 job 객체를 통해 join()을 하게되면 job 객체가 완료될 때까지 기다리게 된다.
만약 위처럼 job이 10개 된다면.... 관리하기가 힘들겠다. 이를 해결하는 방법으로 Structured concurrency를 사용하면 된다.
runBlocking에 대한 자세한 내용은 이 곳에서 확인이 가능하다.
'Coroutine' 카테고리의 다른 글
Coroutines - Part. 5 안드로이드에서의 코루틴 - 동기식과 비동기식, 콜백과 코루틴 (0) | 2021.01.30 |
---|---|
Coroutines - Part. 4 안드로이드에서 코루틴 - ViewModel, Activity, Fragment (0) | 2021.01.30 |
Coroutines - Part. 3 withTimeout(), withTimeoutOrNull() (0) | 2021.01.30 |
Coroutines - Part. 2 취소, 취소할 수 없는 블럭 (0) | 2021.01.30 |
Coroutines - Part 1. 완료 대기, 지연 실행 (0) | 2021.01.30 |