반응형
Coroutine
의 구성요소
Coroutine
은 3가지 요소로 이루어져있다고 볼 수 있다.
CoroutineScope
Coroutine
을 제어할 수 있는 범위,Coroutine
이 활동할 수 있는 범위를 뜻하며,
여기서 제어는 어떤 작업을 취소하거나, 끝날 때까지 기다리는 것을 뜻한다.
CoroutineContext
Coroutine
이 실행된Context
로Coroutine
의 실행 목적에 맞게, 실행될특정 Thread Pool
지정
Builder
Coroutine
을 실행하는 함수. 종류로는 위에서 언급한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
를 지정할 수 있으며,CoroutineContext
param
을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.Main
Main 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
대신 유용하게 사용할 수 있다.
withTimeout
Coroutine
이 정해진 시간안에 실행되지 않으면 예외를 발생시키도록 한다.
withTimeoutOrNull
Coroutine
이 정해진 시간안에 실행되지 않으면 null을 결과로 돌려준다.
awaitAll
- 모든 작업의 성공을 기다린다.
- 작업 중 어느 하나가 예외로 실패하면
awaitAll
도 그 예외로 실패한다.
joinAll
job
이 여러개 인 경우, 이를 이용해 모든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
,Default
Dispatcher
들이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 |