코틀린&안드로이드 관점(?)으로 코루틴에 대해 정리하였다.
👀 Stackless와 Stackful 코루틴 참고
코루틴은 stack의 유무에 따라 두가지 종류로 나뉘며, 코틀린의 경우 stackless하게 구현되어 있다.
- Stackful : 일반적인 함수처럼 내부에서 다른 함수를 호출했을때 해당 코루틴은 suspend할 수 있음.
- Stackless : 호출하려는 함수를 다시한번 코루틴 객체로 묶어서 중첩호출을해야 이전 코루틴과 내부코루틴은 suspend를 통해 연결할 수 있음.
👀 경량?
기존의 스레드는 생성, 해제(GC), Context-switching시 CPU와 메모리를 소모하기 때문에 많은 수의 스레드를 갖기 어렵다.
코루틴은, 스레드가 아닌 서브루틴을 일시중단(suspend)하는 방식이기 때문에 Context-switching에 비용이 들지 않는다.
👀 Context-switching
스레드 실행 혹은 종료시 스레드의 상태를 저장하고 복구하는 프로세스
= 두개의 스레드에서 작업 시 CPU가 매번 스레드를 점유했다가 놓아주는 것을 반복할때 비용 발생
새로운 코루틴을 생성, 시작 실행하기 위해서 Coroutine Scope와 Coroutine Builder가 필요하며 생성시 코루틴에 대한 정보는 Coroutine Context에 담는다.
- 사용할 Dispatcher 결정
- Dispatcher를 이용하여 Scope생성
- Scope의 launch나 async에 수행할 코드블록 작성
runBlocking {
val parent = launch {
GlobalScope.launch {
println("job1 : GlobalScope에서 코루틴 실행")
delay(1000)
println("job1 : cancel 이후에도 호출될까?")
}
launch(Dispatchers.IO) {
println("job2 : 부모의 스코프에서 자식 코루틴 실행")
delay(1000)
println("job2 : cancel 이후에도 호출될까?")
}
}
delay(500)
parent.cancel()
delay(1000)
println("어디까지 출력이 될까?")
}
--------------------------------------
job1 : GlobalScope에서 코루틴 실행
job2 : 부모의 스코프에서 자식 코루틴 실행
job1 : cancel 이후에도 호출될까?
어디까지 출력이 될까?
parent를 중지하더라도 job1은 GlobalScope에서 실행한 코루틴이기 때문에 parent의 자식이 아니다.
따라서, parent을 취소해도 job1은 취소되지 않는다.
job2의 디스패처가 다르더라도 parent의 자식job이기 때문에 같이 취소된다.
프로그램 어디서나 제어 동작이 가능한 범위
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
특정한 목적의 dispatcher를 지정하여 제어 및 동작이 가능한 범위
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
👀 Android환경에서 생명주기에 맞게 제공하는 Custom Scope
- ViewModelScope : viewmodel이 삭제되면 자동으로 취소된다.
- LifecycleScope : Lifecycle객체에서 정의되며 Lifecycle이 끝날때 취소된다.
- Livedata : Livedata Builder에서 suspend 함수를 사용할 수 있으며, 비활성화가 되면 구성가능한 제한시간 후 자동으로 취소된다.
fun <T> liveData( context: CoroutineContext = EmptyCoroutineContext, timeout: Duration, @BuilderInference block: suspend LiveDataScope<T>.() -> Unit ): LiveData<T> = CoroutineLiveData(context, timeout.toMillis(), block){...}
코루틴을 어떻게 처리할 것인지에 대한 여러가지 정보의 집합
코루틴의 실행을 특정 스레드로 제한하거나, 스레드풀에 보내거나, 제한을 받지 않는 상태로 실행할 수 있다
GlobalScope는 재귀적으로 Job이 취소되지 않는다.
👀 헷갈리는 동작순서
내부에서 launch등 새로운 코루틴을 생성할 경우 같은 스레드라도 순서가 하나 뒤로 밀릴 수 있다.(한 프레임 늦게 실행됨)runBlocking<Unit> { // MainThread에서 1 2 3 4 5 순서대로 출력된다. println("${Thread.currentThread().name} 1") launch{ println("${Thread.currentThread().name} 3") delay(1000) println("${Thread.currentThread().name} 5") } println("${Thread.currentThread().name} 2") delay(1000) println("${Thread.currentThread().name} 4") }
- launch가 아닌 withContext로 실행할 경우, 순서를 보장한다.(suspend)
- Dispatchers.Main.Immediate 를 사용하면 바로 실행된다.
👀 CommonPool 사용
이미 초기화되어 있는 스레드 중 하나를 선택함으로써 스레드를 별도로 생성하는 오버헤드가 없어 빠르다.
- 하나의 스레드에 다수의 코루틴이 동작할 수 있다.
- 특정 스레드개수를 직접 지정할 수 있다.
👀 Dispatchers.IO와 Dispatchers.Default
특정 Dispatchers가 더 빠르다라고 정의할 수 없다.
코어수만큼 스레드를 생성하는 Default에는 CPU연산이 많은 무거운 작업을 수행하는 것이 효율적이다.
응답대기시간이 있고, 가벼우면서, 동시에 처리해야하는 작업의 개수가 많은 네트워크 입출력의 작업등은 IO에서 수행하는 것이 효율적이다.
👀 Structured concurrency(구조화된 동시성)
- scope로 생성된 job에서 해당 scope를 사용하여 생성된 모든 코루틴은 job의 자식이 되며, 자식코루틴 중 하나가 처리되지 않은 예외를 던지면 부모job이 취소되어 결국 모든 자식 코루틴이 취소되는 것.
- cancel 또는 예외로 인한 실패로 부모가 취소되면 모든 자식이 즉시 취소된다.
- 부모는 자식이 완료 또는 취소상태로 완료될때까지 기다린다.
- launch로 생성한 코루틴의 경우 자식에서 catch하지 않은 예외는 부모를 취소시킨다.
(async로 생성한 코루틴은 결과에 캡술화 되기 때문에 캐치되지 않는 예외가 없다.)
👀 SupervisorJob
- 한방향으로만 취소를 전달한다.
- 한 자식에서 취소가 발생해도 다른 자식들에게는 영향을 미치지 않는다
- Structured concurrency의 예외사항이다.
디버깅에 유용한 코루틴의 이름
포착되지 않은 예외처리
👀 코루틴 블락을 생성하는 방법(스레드를 기준으로)
1. launch : non-blocking
2. async : non-blocking
3. runBlocking : blocking
launch와 async의 공통점
CoroutineScope의 확장으로, Coroutine Scope가 있어야 새로운 코루틴을 생성할 수 있다.
- 새로운 코루틴을 만든다.
- 하나의 Dispatcher를 갖는다.
- 스코프 안에서 실행된다.
- suspend함수가 아니다.
public fun CoroutineScope.launch(...):Job {...} public fun <T> CoroutineScope.async(...):Deferred<T> {...}
(빌더의 하나로 보는게 맞을까..?)
코루틴 내부나 루틴의 대기가 가능한 곳(runBlocking)에서만 사용이 가능하다.
// 동기적으로 실행하기.
job.join()
job.start()
deferred.await() //해당 코루틴이 종료될때까지 대기 후 다음 라인 실행
deferred.start() //동시에실행?
cancel()을 이용하여 중단할 수 있다.
👀 코루틴 중단시 마무리 처리
- 코루틴을 취소할때, resource를 닫는다거나 마지막으로 해야할 작업이 필요하면 try-finally 구문을 사용하거나 onCancellationException를 사용한다.
- 부득이하게 finally에서 suspending func을 사용해야하면 withContext(NonCancellable)로 감싸준다.
코루틴은 잠시 실행을 멈추거나(suspend) 다시 실행될(resume) 수 있기 때문에 코루틴에서 실행할 수 있는 메소드를 만들때 suspend 키워드를 사용한다..
👀 CPS(Continuation Passing Style)
Kotlin의 코루틴은 suspend 키워드로 마킹된 함수를 CPS(Continuation Passing Style)로 변환하고, 이를 Coroutine Builder를 통해 적절한 스레드 상에서 시나리오에 따라 동작하도록 구성된다.
주의해야 할 점은 suspend function은 스레드와 스케쥴의 관리를 수행하는 것이 아니라 비동기 실행을 위한 중단(suspension) 지점의 정의라는 점이다. 코루틴은 중단 지점까지 비선점형으로 동작하기 때문에 실행 스케쥴이 OS에 의해 온전히 제어되는 스레드와는 다른 관점에서 보아야 한다.
코루틴이 하나의 실행-종료되어야 하는 일(Job)이라면 스레드는 그 일이 실행되는 곳이다.
코루틴과 스레드는 모두 동시성을 보장하기 위한 기술이다.
하나의 스레드에 여러개의 코루틴이 동시에(concurrency) 실행될 수 있다.
스레드는 여러스레드가 있다면 병렬로(Parallelism) 실행된다.
코루틴의 경우 스레드풀을 사용하는 dispatcher로 코루틴들을 실행하면 스레드처럼 병렬프로그래밍을 할 수 있다.
👀 동시성(Concurrency)과 병렬성(Parallelism)
- 동시성 : 시분할, 각 task들을 조금씩 나누어서 실행
- 병렬성 : 병렬수행, 각 task들을 동시에 수행
🙋♀️ ThreadLocal을 사용할 때 주의사항
코루틴 내부에서 사용되는 localdata를 지정할 수 있다.
하나의 로직이 같은 스레드 내에서 실행된다는 보장이 없으므로 동기화에 유의하여야 한다.