반응형
Coroutine의 구성요소

Coroutine은 3가지 요소로 이루어져있다고 볼 수 있다.
CoroutineScopeCoroutine을 제어할 수 있는 범위,Coroutine이 활동할 수 있는 범위를 뜻하며,
여기서 제어는 어떤 작업을 취소하거나, 끝날 때까지 기다리는 것을 뜻한다.
CoroutineContextCoroutine이 실행된Context로Coroutine의 실행 목적에 맞게, 실행될특정 Thread Pool지정
BuilderCoroutine을 실행하는 함수. 종류로는 위에서 언급한launch,async등이 있다
CoroutineScope
Coroutine의 실행되는 범위로Coroutine 블록을 묶음으로 제어할 수 있는 단위이다.- 모든
Coroutine은Scope 내에서 실행되어야 한다.
따라서 이를 통해Activity또는Fragment의Lifecycle에 따라 소멸될 때,관련 Coroutine을 한번에 취소할 수 있고, 이는 곧 Memory Leak을 방지한다.- 즉,
CoroutineScope 객체는 (1) Coroutine을 시작하는 곳이기도 하지만, (2) 모든 Coroutine을 관리하는곳이기도 하고, (3) Coroutine을 끝내는 곳이기도 하다.
→ 언제 끝날지 모르는 어떤 비동기 코드도, Scope가 끝나면 다 끝이다.- 설사, 다른 라이브러리가
Coroutine을 실행시켰고, 그것이 비동기로 돌아가고 있더라도 그 라이브러리가 존재하는Scope가 끝나버리면, 그 안의Coroutine작업도 다cancel되어버린다.
→Android와 같이LifeCycle에 따라 Resource에 대한 Release가 중요한 Framework에서는 매우 편리한 부분이기도 하다.Scope가 끝나면 모든 작업이 끝난다는 것을 보장해주기 때문.
- 설사, 다른 라이브러리가
- Memory Leak을 피하기 위해서는,
Activity가 파괴될 때(onDestroy에서)Coroutine을 취소해줘야 한다. (scope.cancel()) - 모든
Coroutine은 항상자신이 속한 Scope를 참조해야 하고,cancel로 모두 취소가 가능하다
- 즉,
CoroutineScope의 종류
Scope는 커스텀된 또는 이미 내장된 범위를 사용할 수 있다.CoroutineScope는 크게GlobalScope와사용자 지정 CoroutineScope로 나눌수 있다.GloablScope→ Application 프로세스의 Lifecycle를 따라간다사용자 지정 CoroutineScope→CoroutinScope(ConroutineContext)CoroutineScope(Dispatchers.Main)...
- 이외 Android에서의 Scope
MainScope→ UI 관련 작업을 처리하는 용도- 결국
CoroutineScope를 활용하고 있음
- 결국
ViewmodelScope→ViewModel의 Lifecycle를 따라간다.- 따라서,
해당 Scope로 실행되는Coroutine은ViewModel Instance가 소멸될 때 자동으로 취소된다. - 이는 Jetpack Architecture의
ViewModel Component사용시,ViewModel Instance에서사용하기 위해, 제공되는Scope이다. viewModelScope은 스택오버플로우를 찾아본 결과,메인 쓰레드를 사용하게끔 설계되어있었다고 한다.
- 따라서,
LifecycleScope→Activity,Fragment의 Lifecycle를 따라간다.Lifecycle별로 Callback이 다르다.
ViewModelScope와LifecycleScope는 Android Lifecycle용 해당한다
(Lifecyce-Aware Coroutine Scope)- Andorid Jetpack 라이브러리(AAC)에서
Coroutine을 쉽게 사용할 수 있도록 각Lifecycle에 맞는Scope를 제공해주고 있는 것이다. Activity가 종료될 때실행중인 Coroutine도 함께 종료되길 원하면,Activity의 Lifecycle과 일치하는Scope에Coroutine을 실행시키면 된다.
- Andorid Jetpack 라이브러리(AAC)에서
- 이외
CoroutineScope Interface를 구현해Custom한 CoroutineScope를 만들수도 있다.
→ 공식 문서에서 추천하는 방법은 아니라고 한다.
GlobalScope
GlobalScope.launch { //DO SOMETHING
}
binding.button.setOnclickListener {
CoroutineScope(Dispatchers.IO).launch { // DO SOMETHING
}
}
Application의 LifeCycle에만 제한을 받는다. 즉Application의 Lifecycle과 함께 동작하기에 실행 도중에 별도의 생명주기 관리가 필요 없고, 시작-종료 까지 긴 기간 실행되는Coroutine의 경우에 적합하다.- 따라서 이
Scope는 Application이 종료될 때까지Coroutine을 실행시킬 수 있다.Android의 경우 만약Acitivity에서Coroutine을GlobalScope영역에서 실행시킬 경우,Activity가 종료되어도,Coroutine은 작업이 끝날때까지 동작한다- 그럼에도, 저사양 기기에서는 메모리 문제로,
finish()와 함께process가 kill될수도 있으니, 반드시coroutine동작이 유지된다고 판단할 수도 없다.
- 그럼에도, 저사양 기기에서는 메모리 문제로,
GlobalScope에서 수행하는Coroutine은Daemon Thread와 같다. 이는 Process가 kill되면, 같이 멈춘다는 것을 의미한다. (DaemonThread처럼일반 Thread가 모두 종료되면 함께 종료)- 예시
fun main() = runBlocking {
GlobalScope.launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L) // just quit after delay
}
- 위 코드 실행시,
I'm sleeping $i ...은 3개만 찍히고 끝난다. runBlocking은 내부에서 발생한 모든 자식corouitne의 동작을 보장한다.- 내부에서
GlobalScope을 이용하여launch했기 때문에runBlocking과는다른 scope을 갖게된다.(runBlocking의자식 coroutine이 아님) - 따라서
runBlocking은 1.3초만 대기하고 종료하고,main 함수가 종료되면서application process역시 종료된다. - 이에 따라
GlobalScope의 로직도 종료된다. GlobalScope에서launch된active Coroutine은process를 살아있게 하는 역할을 하지 않는다.
사용자 지정 CoroutinScope
버튼을 누르는 등의 동작을 통해 서버에서 이미지를 여는 등의 필요할 때만 열고 완료되면 닫아주는 것
- 매번 원하는 형태의
CoroutineContext를 정의할 수 있다. Coroutine의 생명 주기를 관리할 수 있다.- 필요할 때만 선언하고 종료시키고자 할때, 현재화면을 벗어나면 더이상 필요하지 않을때 사용할 수 있다.
Coroutine Context와 Dispatcher
CoroutineContext의 중요성
launch와async는 모두CoroutineScope의 확장함수이다.- 그런데
CoroutineScope에는CoroutinCotext타입의 필드 1개만 들어있는데, 사실CoroutineScope는CoroutineContext필드를launch등의 확장 함수 내부에서 사용하기 위한 매개체 역할만을 담당한다. - 원한다면,
launch등에CoroutineContext를 넘길수도 있다는 점에서 실제로,CoroutineScope보다CoroutineContext가Coroutine실행에 더 중요한 의미가 있음을 유추할 수 있다.
CoroutineContext란?
CoroutineContext는Context를 어떻게 처리할 것인지에 대한 정보 집합(Element Set)으로,
종류는Dispatcher(Main Element)와Job,ExceptionHandler가 여기에 속한다- 각각의
Element를get혹은fold를 통해 추가하거나 꺼내올 수 있다 CoroutineContext는Coroutine이 어떻게 실행되고 동작해야 하는지를 정의할 수 있게 해주는 요소들의 그룹이다.
- 각각의
CoroutineContext의 의미와 Dispatcher
CoroutineContext는 실제로Coroutine이 실행중인 여러 작업(Job 타입)과 Dispatcher를 저장하는, 일종의 맵이라 할 수 있다.Context는 결합이 될 수 있고, 분리하여 제거할 수도 있다.
(→ plus, minus 연산하여 특정 Element를 합치거나 삭제할 수 있음)Combinding context Elements(CoroutineContext결합)CoroutineContext여러 개의 Element를 정의할 필요가 있을 때+ operator를 사용한다.- 예를 들어 명시적으로
특정 dispatcher를 사용하며명시적으로 이름을 지정한 coroutine을launch하려면
launch(Dispatchers.Default + CoroutineName(“test”)){
println(“I’m working in thread ${Thread.currentThread().name}”)
}
/* OUTPUT */
I’m working in the thread DefaultDispatcher-worker-1 @test#2
Kotlin Runtime은 이CoroutineContext를 사용해서 다음에 실행할 작업을 선정하고, 어떻게스레드에 배정할지에 대한 방법을 결정한다.- 아래 예시를 통해 같은
launch를 사용하더라도, 전달하는Context에 따라서로 다른 스레드상에서Coroutine이 실행됨을 알 수 있다.NewSingleThreadContext
launch { // 부모 컨텍스트를 사용(이경우main)
printin ("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch (Dispatchers.Unconfined) { // 특정 스레드에 종속되지 않음 ? 메인 스레드 사용
printin ("Unconfined : I ’m working in thread ${Thread.currentThread().name}")
}
launch (Dispatchers.Default) { // 기본 디스패처를 사용
printin ("Default : I’m working in thread ${Thread.currentThread().name}")
}
launch (newSingleThreadContext ("MyOwnThread”)) { // 새 스레드를 사용
printin ("newSingleThreadContext: I 'm working in thread ${Thread.currentThread().name}")
}
NewSingleThreadContext는 새로운thread를 생성해서 그곳에서 수행한다.- 이는 새로운
Thread를 만들기 때문에 resource 측면에서 가장 비싸다. - 실제 앱에서 더이상 사용하지 않으면,
close function으로 release 시켜줘야한다. 그렇지 않으면, memory leak이 발생할 수 있다. - 또는
Top-Level로 정의해서Application Lifecycle 전반에서 재사용해야한다.
Coroutine의 생성과 Dispatcher
- Kotlin은
쓰레드생성과정을 단순화해서 쉽고 간단하게쓰레드를 생성할 수 있다. Coroutine은Thread와Thread pool을 쉽게 만들순 있지만, 직접 Access하거나 제어하지 않는다.- 해당 Thread pool 제어는 모두
Dispatcher에 맡긴다.- 우리가
Dispatcher에Coroutine을 보내기만 하면,Dispatcher은Thread에Coroutine을 분산시킨다. 이는Dispatcher에서Thread에 할당하는 과정을 통해 알 수 있다. Coroutine dispatcher는Coroutine의 실행을특정 하나의 Thread에 한정시킬 수 있고,Thread Pool에 던질수도 있고,정의되지 않은채로 실행시킬 수도 있다.- 따라서 모든
CoroutineBuilder(launch,async등)들은Dispatcher를 지정할 수 있으며,CoroutineContextparam을optional로 받는다param이 없는launch의 경우 실행되는CoroutineScope의context
(그 안의dispatcher도 당연히)를 상속한다
- 우리가
- 결국
Coroutine에서의 핵심은Light-Weight Thread를 어떻게 관리할 것인지 인데,
이를 담당하는 것이 바로Dispatcher이라는 것이다.CoroutineContext를 상속받아,어떤 쓰레드를 이용해 동작할 것인지 미리 정의되어 있다.
(Coroutine실행에 사용하는쓰레드를 결정한다)
- 해당 Thread pool 제어는 모두
Coroutine을 적당한 Thread에 할당하는 과정
- 유저가
Coroutine생성 후,Dispatcher에 전송
Dispatcher는자신이 잡고있는 Thread pool에서자원이 남는 Thread가어떤 Thread인지 확인한 후,해당 Thread에Coroutine을 전송한다
분배받은 Thread는해당 Coroutine을 수행한다
- 여기서
CoroutineDispatcher을 만들어야 하는데, 기본적으로 가용성, 부하, 설정을 기반으로 **Thread간에**Coroutine을 분산하는orchestrator이다.
(Coroutine에 대한Task수행 분배를 어떻게 할것인지 결정하는 역할)
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
/** @suppress */
@ExperimentalStdlibApi
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
@InternalCoroutinesApi
public open fun dispatchYield(context: CoroutineContext, block: Runnable): Unit = dispatch(context, block)
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
public final override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
val dispatched = continuation as DispatchedContinuation<*>
dispatched.release()
}
}
- 추상 클래스로 지원하고 있다.
dispatch라는 메소드를 통해실행가능한(Runnable)한 task를구현체에서 구현된 방식을 토대로 task를 던짐으로써Coroutine에 대한 동작을 실행하게 되는 것이다.Task를 관리하는구현체중 하나인EventLoop코드를 보게되면
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)
public fun enqueue(task: Runnable) {
if (enqueueImpl(task)) {
// todo: we should unpark only when this delayed task became first in the queue
unpark()
} else {
DefaultExecutor.enqueue(task)
}
}
Dispatcher는 정말 말 그대로task를 EventLoop Queue에 넣고 끝내는 작업만 진행한다.- 그러면
Event Loop를 관리하는Thread에서 하나씩Deque하여Task를 꺼내 실행하게 되는 것이다.
즉 Disptacher는 Coroutine이 동작하는 방식하고 연관이 있는 것이 아닌 Task에 대한 분배에 대한 책임만 있다.
- 또한,
Custom Threadpool을 위한Dispatcher도 생성할 수 있다.Executors라이브러리를 이용해,Custom으로 만든 Thread pool에도 지정이 가능- 예시 (
우선순위가 높은 Thread pool에서 동작하는CoroutineScope를 만든 예)
val customExecutor: Executor = Executors.newCachedThreadPool { r ->
Thread(r, "CustomThread").apply {
priority = Thread.MIN_PRIORITY
}
}
val customDispatcher = object : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
customExecutor.execute(block)
}
}
CoroutineScope(customDispatcher).launch { }
Dispatcher가 만들어지면 이를 사용하는Coroutine을 시작할 수 있다.
대표적인 Dispatchers
Android에서는 상황에 맞는Dispacther가 이미 생성되어있다.
Dispatchers.Default- 복잡하고 오래 걸리는 작업 (CPU를 많이 사용하는 작업 - 데이터 정렬, 복잡한 연산)에 최적화되어 있다.
- 공유된
Background Thread Pool을 사용한다. (Rx의Computation Scheduler을 생각) launch(Dispatacher.Default){ ... } 와GlobalScope.launch{ .. }는 동일한Default Dispatcher를 사용한다.
Dispatchers.IO- 파일 입출력, API Call(네트워크, 디스크, DB 작업)에 최적화되어 있다.
Dispatchers.MainMain Thread작업(UI와 상호작업) 및Non-Blocking코드에 최적화되어 있다.Android Main Thread에서Coroutine을 실행하는Dispatcher이다.
Dispatchers.Unconfined호출한 Context를 기본으로 사용하는데, 중단 후 다시 실행될 때Context가 바뀌면,바뀐 Context를 따라가는 특이한Dispatcher이다.- (1)
Coroutine이 CPU 시간을 사용하지 않고
(2) UI를 비롯한 특정 쓰레드에 제한된 공유 데이터(shared data)를 업데이트 하지 않는 경우에 사용하는 것이 적절하다. 호출한 Thread에서Coroutine을 수행(시작)하지만 1번째 suspension 지점까지만 유효하다.- suspension이 끝나면,
호출된 suspension 함수에의해다른 Thread로Coroutine을 재개한다.
(suspension이resume될 때는suspending function을 수행된thread에서resume된다) - 예시
runBlocking{
launch(Dispatchers.Unconfined){
print(“Unconfined : I’m working in thread ${Thread.currentThread().name}”)
delay(500L)
print(“Unconfined : After delay in thread ${Thread.currentThread().name}”)
}
launch{
print(“Main : I’m working in thread ${Thread.currentThread().name}”)
delay(1000L)
print(“Main : Aftrer delay in thread ${Thread.currentThread().name}”)
}
}
/* OUTPUT */
Unconfined : I’m working in thread main
Main : I’m working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor
Main : After delay in thread main
- Unconfind의 경우, delay가 DefaultExecutor에 의해 호출되기 떄문에, resume은 DefaultExecutor에서 된다.
- Unconfined Dispatcher는 고급 Machanism으로 특정 case에 유효하다. 따라서 일반적인 코드에서는 쓰지 않는 것이 좋다.
Dispatcher 전환하면서 Coroutine 붙이기
Main Thread에서Coroutine을 실행하되, 몇몇Coroutine은다른 Thread에서 실행하도록 할 수도 있다.
CoroutineScope(Dispatchers.Main).launch { // 0. Main Dispatcher를 기본으로 설정
// 1. 데이터 입출력을 해야 하므로 IO Dispatcher에 배분
val deferredInt: Deferred<Array<Int>> = async(Dispatchers.IO) {
println(1)
arrayOf(3, 1, 2, 4, 5) // 마지막 줄 반환
}
// 2. Sort해야 하므로 CPU작업을 많이 해야하는 Default Dispatcher에 배분
val sortedDeferred = async(Dispatchers.Default) {
val value = deferredInt.await()
value.sortedBy { it }
}
// 3. 설정하지 않으면 기본 Dispatcher인 Main Dispatcher에 보내진다.
// 3. TextView에 세팅하는 것은 UI 작업이므로 Main Dispatcher에 배분
val textViewSettingJob = launch {
val sortedArray = sortedDeferred.await()
setTextView(sortedArray)
}
}
→ Coroutine 생성시, Disptacher을 설정하는 것만으로도 Dispatcher 전환이 가능하다
ExceptionHandler
- 만약 아래처럼,
Coroutine안에서Exception이 발생하면?
GlobalScope.launch(Dispatchers.IO) {
launch {
throw Exception()
}
}
→ Application이 갑자기 죽을 것이다.
- 이는
CoroutineExceptionHandler를 이용하여 예외처리 할 수 있다. - 자세한 내용은 이후의 예외처리 내용을 참고한다.
Coroutine 생성 과정
- 사용할
Dispatcher결정 Dispatcher를 이용해CoroutineScope를 만든다CoroutineScope의launch나async에 수행할 코드블럭을 넘긴다
- 예시
GlobalScope.launch {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
CoroutineBuilder와 일시중단 함수
CoroutineBuilder
- 이전에 나온
launch나async,runBlocking은 모두CoroutineBuilder라고 불린다. 이들은Coroutine을 만들어주는 함수이다. - 이외에도
kotlinx-coroutines-core 모듈이 제공하는CoroutineBuilder는 아래와 같이 2가지가 더있다produce- 정해진 채널로 데이터를
Stream으로 보내는 코루틴을 만든다 - 이 함수는
ReceiveChannel<>을 반환한다. - 그
채널로부터 메시지를 전달받아 사용할 수 있다.
- 정해진 채널로 데이터를
actor- 정해진
채널로 메시지를 받아 처리하는Actor를 코루틴으로 만든다. - 이 함수가 반환하는
SendChannel<>채널의send()메소드를 통해Actor에게 메시지를 보낼 수 있다.
- 정해진
Coroutine Builder의 2가지 flavor
- Exception을 자동으로 전파
launch,actor- 자동 전파하는 케이스는
Java의uncaughtExceptionHandler와 비슷하게unhandled exception launch로 생성한Coroutine의 경우 자식에서 catch하지 않은 예외는 부모를 취소시킨다- 기본적으로
Job내부에서 발생하는 예외는Job을 생성한곳까지 전파되기 때문에, 완료되기를 기다리지 않아도 발생한다.
- 유저에게 노출
async,produce- 유저에게 노출되는 경우는 유저가 직접
exception을 직접 핸들링할 수 있다 (try-catch할수있다) - 대표적 API는
await와receive async로 생성한Coroutine은 결과에 캡슐화되기 때문에, Catch되지 않는 예외가 없다.- 예시
예시(예외를 Throw하는 Function인doSomeThing()과async()를 통해task를 실행하는 코드)
runBlocking {
val task = GlobalScope.async {
doSomething()
}
//(1) task.join()
//(2) task.await()
}
fun doSomething() {
throw UnsupportedException("Can't do")
}
join()을 통해,task를 실행하면 에러가 발생하지 않고 성공적으로 실행되지만,await()를 통해 실행 하게되면, 에러가 발생하면서 종료하게 된다.await()의 경우 예외를 감싸지 않고, 전파하기 때문에,upwrapping deferred라고 불린다.- 이처럼
join()으로 대기한 후, 검증하고 오류를 처리하는 것과await()를 직접 호출하는 방식의 차이는 예외 전파의 유무라고 볼 수 있다.
일시중단 함수(Suspending Function)
- 해당 함수들은
Coroutine 내부혹은runBlocking()과 같은Routine의 대기가 가능한 구문안에서만 동작이 가능하다. delay()와yield()와 같은 함수들도일시중단 함수라고 부른다.- 이외에도
kotlinx-coroutines-core모듈의최상위에 정의된 일시 중단 함수는 아래와 같이 더 있다.withContext부모 Coroutine에서 사용되던Context와다른 Context로Coroutine을 전환한다Coroutine에서 결과를 반환할 때async대신 유용하게 사용할 수 있다.
withTimeoutCoroutine이 정해진 시간안에 실행되지 않으면 예외를 발생시키도록 한다.
withTimeoutOrNullCoroutine이 정해진 시간안에 실행되지 않으면 null을 결과로 돌려준다.
awaitAll- 모든 작업의 성공을 기다린다.
- 작업 중 어느 하나가 예외로 실패하면
awaitAll도 그 예외로 실패한다.
joinAlljob이 여러개 인 경우, 이를 이용해 모든Coroutine이 완료되는 것을 기다릴 수 있다.- 모든 작업이 끝날 때 까지 현재 작업을 일시 중단시킨다.
withContext로 join(), await() 대체
withContext는Thread간 Jump에 사용된다.
따라서,withContext를 사용함으로써사용하는 Thread 변경가능하다Coroutine은 쓰레드로부터 독립적(Thread independent)이기에,Main Thread에서하나의 Coroutine을 시작하고, 이것을다른 SubThread1으로 보내고, 또SubThread2로 보냈다가, 다시MainThread에서 작업하는게 가능하다.
→ 이렇게Context Switching을 해주는 것이withContext의 역할이다- 기존에 다른
Coroutine에 보내진 작업을 수신하려면 다음과 같은 코드가 필요했다
suspend fun main() {
val deferred: Deferred<String> = CoroutineScope(Dispatchers.IO).async {
"Async Result"
}
val result = deferred.await()
println(result)
}
→ Deferred로 결과값을 감싼 다음, await() 메소드를 통해, 해당 값이 수신될때 까지 기다려야한다
suspend fun main() {
val result: String = withContext(Dispatchers.IO) {
"Async Result"// 반환 값
}
// result = "Async Result"
println(result)
}
→ 위와 같이 withContext를 통해 코드양을 줄일수 있게 되었다.
withContext()는async와 동일한 역할을 하는 키워드인것을 알 수 있다.- 차이점은 (1)
await()를 호출할 필요가 없으며
(2)마지막 구문에 해당하는 결과가return될 때까지 기다린다. - 즉 프로세스에
Job을 포함시키지 않고도다른 Context로 전환할 수 있게 해주는일시중단 함수이다. (참고로 이는,withContext내부적으로async{}. await()로 구현되어 있어 내부 코드가 모두 실행된 다음 다음 코드로 넘어가기 때문이다.)
withContext()의 효용
suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T (source)
- 예시를 통해 알 수 있는,
withContext의 2가지 특성withContext블럭의 마지막 줄의 값이 반환값이 된다withContext가 끝나기 전에 해당 Coroutine은 일시 정지
- 이러한 특성으로
withContext를 이용하면, 비동기 작업을 순차코드처럼 작성할 수 있다. - 기본적으로
부모 Coroutine의Dispatcher을 사용하지만,withContext로Dispatcher를 달리 사용할 수 있게 되는 것이다.- 예시 1
CoroutineScope(Dispatchers.Main).launch {
// ui 처리 코드
// ...
val result = withContext(Dispatchers.IO){
readFile()
}
Log.d("코루틴", "$result")
}
-
- 예시2 (
I/O Thread에서 작업후, 순차적으로Main Thread에서 보여주기 위한것)
- 예시 3
- 예시2 (
GlobalScope.launch(Dispatchers.IO) {
Log.d(TAG, "Do something on IO thread") // 1 IO 쓰레드에서 실행
val name = withContext(Dispatchers.Main) { // 2 Main 쓰레드에서 실행
sleep(2000)
"My name is Android"
}
// 3 withContext() 다음 코드를 수행하지 않음.
//await()을 호출한 것처럼 결과가 리턴되기를 기다림
Log.d(TAG, "Result : $name")// 4 withContext()의 코루틴이 모두 수행되면 이 코드가 수행
}
/* OUTPUT */
10-26 22:44:16.488 9723 9752 D MainActivity: Do something on IO thread
10-26 22:44:18.649 9723 9752 D MainActivity: Result : My name is Android
- 주의할 점은
Main,I/O,DefaultDispatcher들이suspend나Resume을 할때,suspend한 시점의thread에서 정확하게resume하지 않을 수 있으므로,thread안에서만 유효한 변수를 사용할 때는 주의를 기울어야 한다.
withTimeoutOrNull
- 수동으로
Job을 직접Referencing하면서, 특정 시간 이후에 취소시킬수도 있지만,Coroutine은 이럴때, 사용하기 쉬운 함수인withTimeoutOrNull을 제공해준다. - 만약 네트워크 호출을 했는데, 서버의 상태가 좋지 못하거나, 디스크 입출력을 하다 에러가 발생하는 경우,
해당 작업을 취소하고 유저에게 알려주는 등의 작업을 해야한다- 이러한 이유로 실제 Product에서 중요한 부분은 Timeout과 관련된 부분이기에 이 함수를 활용한다
- 인자에 시간을 넣어주고, 메소드를 실행시켜준다
- 해당 시간이 지나도 정상적으로 종료되지 않으면,
null을 반환하므로,
이null값을 이용해 유저에게 정보를 알려줄 수 있다.
- 해당 시간이 지나도 정상적으로 종료되지 않으면,

withTimeout
- 마찬가지로 실행시간이 timeout을 넘겨,
Coroutine의 취소하는 경우에 사용된다.
/* Coroutine에 Timeout을 설정하고 이 시간을 넘어설 경우 Coroutine이 취소되도록 구현하는 것을 Coroutine 기본 함수로 구현할 경우 */
fun main(args: Array<String>) = runBlocking<Unit> {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("main : I'm running finally!")
}
}
launch {
delay(1300L)
println("main : I'm tired of waiting. Cancel the job!")
if (job.isActive) {
job.cancelAndJoin()
}
}
}
// 1. 제한 시간을 설정할 대상이 되는 코루틴을 생성한다.
// 2. 일정 시간(Timeout) 지연 후 전달 받은 Job이 끝나지 않았으면 취소하는 동작을 하는 코루틴을 생성하고, 1번에서 만들고 실행한 코루틴의 Job 객체를 전달한다.
// 3. 테스트를 위해 1번 코루틴은 2번 코루틴에서 설정한 시간보다 긴 수행시간을 갖도록 구현한다.
fun main() = runBlocking{
withTimeout(1300L){
repeat(1000){ i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
// job: I'm sleeping 0 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
TimeoutCancellationException은withTimeout에서 던지며, 이 녀석은CancellationException의subclass이다.- 이런 stack trace를 이전에 본 적이 없는데, 그것은
취소된 coroutine에서 던지는CnacellationException은Coroutine종료의 일반적인 동선으로 보기 때문이다. - 하지만
withTimeout함수를main Function안에서 직접 사용했기 때문에Exception이 발생하는 것이다. - 취소는 단순히
Exception이기 때문에, res들을 추가로 정리할 것이 있다면, timeout 코드를 try{ .. } catch(e:TimeoutCancellationException) { ... } 블럭으로 감쌀 수 있다. - 그러나 특별히 추가적으로 할 일이 없다면,
withTiemoutOrNull로 감싸주면되며, 이는withTimeout과 비슷하지만, timeout시 exception을 던지는 대신null을 return 하기 때문이다.
newSingleThreadContext를 만들어 Thread간 Jumping
newSingleThreadContext(“Ctx1”).use{ ctx1 ->
newSingleThreadContext(“Ctx2”).use{ ctx2 ->
runBlocking(ctx1){
log(“Started in ctx1”)
withContext(ctx2){
log(“Working in ctx2”)
}
log(“Back to ctx1”)
}
}
}
/* OUTPUT */
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
withContext블록은 같은coroutine안에 머물러 있다는 것을 눈여겨보자.newSingleThreadContext로 만든 것은close를 불러주어야 하는데,kotlin 의 use를 사용하면 이를 자동으로 할 수 있다.
반응형
'IT > Kotlin' 카테고리의 다른 글
| [Kotlin/Coroutine] 6. Coroutine의 Structured Concurrency(구조화된 동시성) (0) | 2023.05.19 |
|---|---|
| [Kotlin/Coroutine] 5. Coroutine의 예외처리와 취소 (0) | 2023.05.08 |
| [Kotlin/Coroutine] 3. launch와 async의 Job과 Deferred (0) | 2023.04.07 |
| [Kotlin/Coroutine] 2. Kotlin에서의 Coroutine: launch와 async (0) | 2023.03.24 |
| [Kotlin/Coroutine] 1. Coroutine이란 무엇인가 (0) | 2023.03.16 |