1. Coroutine이란?
코루틴은 흔히 경량 스레드라고 불리며, 스레드 위에서 실행되는 하나의 일(Job)이라고 이해할 수 있다.
코루틴은 코루틴이 시작된 스레드를 중단하지 않으면서 비동기적으로 실행되는 코드이다.
코루틴은 스레드 위에서 실행되기 때문에 여러가지 코루틴이 존재할 때, 코루틴1이 실행 중 코루틴2가 실행되어도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라, 기존 스레드를 유지하며 코루틴2를 실행하게 된다.
이 후 코루틴1을 다시 실행할 때 저장해둔 코루틴1의 상태를 불러와 다시 스레드에서 코루틴1을 실행하게 된다. 즉, 스레드의 멈춤 없이 루틴을 돌릴 수 있으며 여러 스레드를 사용하는 것보다 훨씬 적은 자원을 소모하게 된다.
2. Coroutine Scope
- GlobalScope : 앱의 생명주기와 함게 동작하기 때문에 실행 도중에 별도 생명 주기 관리가 필요없음. 시작 ~ 종료 까지 긴시간 동안 실행되는 코루틴의 경우에 적합 (안드로이드 앱 개발에서는 거의 사용하지 않음)
- CoroutineScope : 버튼 이벤트로 Download 하거나 서버에서 이미지를 열 때 등, 필요할 때만 열고 완료되면 닫아주는 코루틴 스코프를 사용할 수 있다.
- ViewModelScope : Jetpack 아키텍처의 뷰모델 컴포넌트 사용 시 ViewModel 인스턴스에서 사용하기 위해 제공되는 스코프. 해당 스코프로 실행되는 코루틴은 ViewModel 인스턴스가 소멸될 때 자동으로 소멸된다.
모든 코루틴은 스코프 내에서 실행되어야 하는데 이를 통해서 액티비티 또는 프래그먼트의 생명주기에 따라 소멸될 때 관련 코루틴을 한번에 취소할 수 있는데 이는 곧 메모리 누수를 방지한다.
3. Coroutune Context
코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext 유형의 값으로 표시되는 일부 컨텍스트에서 실행된다.
주요 요소는 코루틴의 Job과 Dispatcher가 있다.
코루틴의 Context는 코루틴이 실행 될 Context로, 코루틴의 실행 목적에 맞게 실행 될 스레드 풀을 지정해 준다.
Dispetchers 종류
- Dispatchers.IO
- 코투틴은 공유된 스레드 풀에 있는 백그라운드 스레드에서 동작한다.
- 로컬 DB와 작업할 때, 네트워크 작업 또는 파일과 관련된 작업을 할 때 사용한다
- Dispatchers.Main
- 코루틴이 메인스레드에서 동작한다.
- UI관련 함수, suspending 함수, LiveData에서 수정사항을 가져오는 함수 등, 작고 가벼운 작업들만 실행 시키는 것이 좋다.
- 코루틴을 메인스레드에서 시작하고 백그라운드 스레드로 변경하는 것이 Best Practice이다.
- Dispatchers.Default
- 많은 리스트를 정렬하는 작업같이 CPU 부하가 많은 작업을 할 때 유용하다.
CoroutineScope(Dispatchers.IO).launch {
//TODO something...
}
위의 예시는 CoroutineScope의 Context로서 Dispetchers.IO를 사용한 경우이다.
val job = Job()
CoroutineScope(Dispatchers.IO + job).launch {
//TODO something...
}
만약 Job instance를 CoroutineScope의 Context로서 사용하고 싶은 경우 job instance의 이름을 포함하면 된다.
'+' 연산자는 다수의 코루틴 컨텍스트를 merge하는데 사용된다.
4. Cotoutine Builder
Coroutine Scope의 확장 함수로서 새로운 코루틴을 실행하기 위해 사용된다.
Coroutine Builder의 종류에는 4가지가 있다.
- Launch
- 현재 스레드를 blocking 하지 않고 새로운 코루틴을 실행한다.
- job 인스턴스를 반환하고 그 인스턴스는 코루틴에 대한 참조로 사용된다.
- 리턴 값이 없는 코루틴을 위해 사용한다.
- 반환 된 job 인스턴스로 코루틴을 제어 할 수 있다. ex) job.join()
- Async
- 현재 스레드를 blocking 하지 않고 새로운 코루틴을 실행한다.
- Deferred<T>의 인스턴스를 반환한다. 값을 얻기 위해서 await() 함수를 사용한다.
- Deferred 인터페이스는 job 인터페이스를 확장한 것이다.
- 결과 값을 반환하고 싶은 경우에 Async Builder를 사용한다.
- produce
- 요소 스트림을 만드는 코루틴을 위한 Builder이다.
- reveiveChannel 인스턴스를 반환한다.
- runblocking
- 다른 Coroutine Builder들과 다르게 작업이 끝날 때 까지 스레드를 Block 한다.
- T 타입의 결과를 반환한다.
val list = ArrayList<Int>()
CoroutineScope(Dispatchers.Main).launch {
val launchJob = CoroutineScope(Dispatchers.Main).launch { //_ 결과 값 리턴하지 않고 해당 코루틴을 제어할 수 있는 job을 리턴
for (i in 0 until 10) {
list.add(i)
}
}
val asyncJob = CoroutineScope(Dispatchers.Default).async { //_ async 블록의 마지막 값이 Diferred로 Wrapping 되며 Differred를 리턴
for (i in 10 until 20) {
list.add(i)
}
val value = list
value //_ == Deferred<ArrayList<Int>>
}
launchJob.join() //_ 코루틴이 완료될 때 까지 대기
Log.d("TEST_LOG", "${list}") //_ 코루틴 완료 된 후 list 출력 / launchJob을 join()을 통해 결과 값을 얻을 때 까지 대기하지 않으면 list는 empty 상태로 로그에 출력
val resultList = asyncJob.await() //_ 코루틴이 완료될 때 까지 대기 / 별도의 delay가 없더라도 Dispatchers.Default 컨텍스트로 얻어진 스레드 풀로부터 결과 값을 수신할 떄 까지 Main Thread가 일시 중지 된다.
Log.d("TEST_LOG", "${resultList}") //_ Diferred에서 await() 메소드를 통해 결과값을 수신하여 출력
Log.d("TEST_LOG", "${list}") //_ 코루틴 완료 된 후 list 출력 / asyncJob을 await()을 통해 결과 값을 얻을 때 까지 대기하지 않으면 list의 결과값은 업데이트 되지 않은채 로그에 출력됨.
}
launch Builder로 반환받은 job 인스턴스는 join() 함수를 통해 코루틴이 완료될 때 까지 기다릴 수 있으며, async Builder로 반환받은
deferred 인스턴스는 await() 함수를 통해 코루틴이 끝날 때 까지 결과를 기다릴 수 있다.