https://developer.android.com/kotlin/coroutines
https://inf.run/gFt8
Coroutines
- Android의 비동기 프로그래밍에 권장되는 Solution
- 코루틴= 비동기적으로 실행되는 코드를 간소화하는 동시 실행 설계 패턴
- 해당 프로세스가 수행되는 동안 기본 스레드를 차단해 앱이 응답되지 않게 만들 수 있는 장기 실행 작업을 관리하는데 도움
- a function that could be started, paused, and resume.
- 단일 스레드에서 많은 코루틴을 실행할 수 있기에 경량화된 동시작업을 수행
- ex) 기본 스레드에서 네트워크 요청 -> 응답을 받을 때까지 스레드 대기 or 차단 -> 앱 정지 상태 = 애플리케이션 응답없음(ANR) ===> 이를 개선하기 위해 코루틴을 만들고 코루틴에서 네트워크 요청 수행
- 'Co' -> with, together
- 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행이 재개될 수 있게 함.
- 중단이 되고 재개가 가능한 루틴
- 콜백지옥을 순차적인 코드로 실행할 수 있게 함, 비동기를 순차적으로 실행할 수 있도록 함
=> 콜백개념을 신경쓰지 않아도 똑같이 동작함.)
- 코루틴이 스레드보다 light하다. (Global coroutines are like daemon threads)
- 코루틴이 무조건적인게 아니라 프로세스가 살아있을 때만(=메인함수가 살아있을 때만) 코루틴이 동작한다.
- 프로세스가 끝나면 코루틴이 돌아가고 있어도 코루틴이 끝난다.
build.gradle
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
- launch = 코루틴빌더 (launch를 통해 내부적으로 코루틴 반환)
- globalScope객체(=전역scope) 안에서 코루틴 실행
- 코루틴 빌더 안에서 수행되는 delay는 suspend function
- runBlocking도 코루틴빌더 (내부적으로 코루틴 반환해 수행)
단, runBlocking은 suspend function X -> Thread가 끝날 때까지 Blocking 하는 것과 동일
- Thread를 Blocking 해야 하는 상황이 생길 때, runBlocking 사용
- 반대로, suspend function은 thread를 blocking 시키지 않음, 코루틴이 수행되는 동안 main thread 죽어버리지 않게 관리가 중요
- 위와 동일한 결과지만, 좀 더 관용적 형태
- runBlocking 안에 있는 구문이 다 실행되기 전까지 main함수 리턴을 막음.
job 코루틴 객체에 join()를 취하면 job코루틴이 완료될 때까지 메인함수가 종료되지 않고 대기.
runBlocking에서 들어온 코루틴Scope로부터 launch를 하여 코루틴빌더를 생성
이렇게 하면 각 코루틴마다 job 객체를 만들어서 각각 join을 할 필요가 없다.
parent 코루틴인 runBlocking의 child로 launch를 이용한 코루틴을 만들면 parent 코루틴이 child 코루틴이 완료되는 것까지 알아서 대기.
suspend 키워드를 붙여줘서 doWorld함수도 일시 중단이 가능한 함수로 만들어줌.
suspend function은 코루틴이나 suspend function안에서만 호출 가능
스케줄링 관점에서 runBlocking -> 첫번째 launch -> 두번째 launch 이렇게 쌓이는 구조
따라서, Coroutine Outer가 먼저 나오게 되고
두 번째로, 첫 번째 launch가 나오게 되고 delay시 suspend되므로 두번 째 launch가 실행됨.
두 번째 launch가 suspend되면 첫 번째 launch가 resume
즉, suspend <-> resume이 반복되는 형태
- 하나의 thread에서 여러 개의 코루틴을 실행할 수 있기에 메모리를 절약하면서 동시성 작업을 수행할 수 있다. (suspend function을 통해 중단된 시점부터 다시 수행할 수 있기에 하나의 thread가 여러 작업을 suspend <-> resume을 반복하며 수행할 수 있는 형태)
-> 따라서, coroutines은 경량화된 비동기 처리를 지원
- 코루틴을 적절히 취소할 수 있는 것이 리소스 활용에 중요한 테크닉
launch할 때 받은 코루틴 객체는 cancel 가능
job(코루틴객체).cancel() // cancel the job (코루틴 종료)
job(코루틴객체).join() // waits for job's completion
코루틴을 완벽히 취소하려면 cancel을 check 해야 된다.
suspend function을 통해 코루틴을 취소하는 구조
위 예제는 코루틴이 취소가 되지 않음. -> why? suspend function이 없기 때문 (delay suspend function이라도 있어야 함.)
suspend function을 사용하지 않고 코루틴을 취소할 수 있는 방법 -> yield() 사용
job.cancelAndJoin() // 캔슬과 조인을 순차적으로 실행시키는 함수
코루틴을 취소 시킨다는건 강제로 Exception을 발생시켜서 코루틴을 중단시키는 원리
그러나, 코루틴 내부에서 exception을 체크할 수 있는 어떠한 suspend function이 없다면 코루틴은 완전히 완료되기 전까지는 중간에 취소 되지 않는다.
- 명시적으로 isActive 상태를 확인해 active상태가 아니면 코루틴 중단시키는 방법
- Exception을 던지지 않기 때문에 더 유연한 방법
코루틴이 중간에 취소 될 경우 finally 작업을 통해 clean up 진행
ex) DB작업 중이거나 파일을 닫고 끝내야 한다던가 등등
- Timeout
launch된 코루틴의 job을 캔슬하는게 아니라 코루틴 실행하면서 일정 시간이 지나면 취소되도록 함
timeout이 되면 exception을 던지면서 죽음
- Sequential by default
비동기를 하게 되면 순서를 맞추기 어려운데
이러한 비동기들을 어떻게 순서대로 처리하는가에 관한 내용
- Concurrent using async
두 suspend 비동기 함수들이 독립적이라면,
순차적으로 실행하지 않고 각각 수행하여 더 빠르게 결과 값을 얻을 수도 있다. ( 두 함수를 동시에 수행하므로 더 빠르게 가능 )
즉, 순차적으로 비동기함수를 수행하는게 아니라 동시에 하고 싶으면 async를 통해 명시적으로 호출
launch와 async는 코루틴 빌더지만, return 객체 차이가 존재
- launch : Job 객체 반환
- async : Deferred< T > 반환
Job 객체는 리턴값을 받을 수 없지만,
Deferred는 async 내에서 수행된 결과를 리턴받을 수 있음
단, Deferred는 job을 상속받았기에 Job객체의 일종.
- Lazily started async
async 코루틴을 나중에 수행할 수 있는 방법
async 자체는 코루틴 빌더
async 파라미터로 LAZY를 넣어주면 코루틴이 바로 수행되는 것을 막고
start()를 통해 코루틴이 실행될 수 있도록 함.
- Structured concurrency with async
exception이 발생하면 모든 코루틴이 취소될 것이다.
일반함수처럼 코루틴 함수를 사용하려면
coroutineScope로 감싸서 사용해야 안전하다는 뜻.
- Cancellation propagated coroutines hierarchy
만약 async로 수행되는 코루틴들이 있을 때 한 코루틴에서 exception이 일어나면
exception이 propagated 되면서 연쇄적으로 취소가 되어짐. ( cancel이 전파됨. )
즉, hierarchy 구조로 실행되던 모든 코루틴들이 취소 됨.
- Coroutines under the hood
- 코루틴이 어떻게 동작하는지에 대한 conf
https://youtu.be/YrrUCSi72E8
KotlinConf 2017 - Deep Dive into Coroutines on JVM by Roman Elizarov
코루틴 Context에서 코루틴이 실행되는데
코루틴 Context 요소에는 Dispatcher가 존재
(코루틴 context에는 해당 코루틴을 실행하는데 쓰이는 thread, Dispatcher가 존재)
Dispatcher는 코루틴이 어떤 스레드, 스레드풀에서 실행될지를 결정함
모든 코루틴 builder들은 옵셔널로 coroutineContext parameter를 받는다.
(launch, async 같은 코루틴 빌더들 -> dispatcher 옵션을 명시적으로 지정할 수 있는 파라미터를 허용)
명시적으로 Dispatcher를 줄 수도, 안줄 수도 있다.
-
결과
Unconfined :I'm working in thread main
Default :I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext :I'm working in thread MyOwnThread
main runBlocking :I'm working in thread main
Process finished with exit code 0
새로운 코루틴의 job은 부모 코루틴의 job의 child가 된다.
단, GlobalScope를 사용하게 되면 별도로 job이 생성되고 부모 자식 관계가 성립하지 않는다.
GlobalScope는 앱 전체를 범위로 하기 때문이다.
- Parental responsibilities
모든 자식 코루틴들의 수행이 완료될 때까지 기다려줌
직접 트래킹할 필요가 없다. 즉, join을 사용하지 않아도 부모 코루틴에서 자식 코루틴을 만들면 자식 코루틴의 완료를 다 기다려준다.
-
Coroutine scope
하나의 Coroutine scope에서 여러 코루틴들을 만들면 나중에 이 Coroutine scope를 종료할 때 엮여있는 모든 코루틴들이 같이 종료될 수 있게 만들 수 있음.
-
결과
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destorying activity
Process finished with exit code 0
정리
- Coroutine builder // 코루틴 생성 및 실행
- launch
- runBlocking
- Scope // 빌더들은 scope안에서 실행 됨.
- GlobalScope -> lifetime이 전역
- CoroutineScope
- Suspend function // 일시중단 함수
- suspend
- delay() -> delay함수도 suspend function(코루틴을 일시중지시킴)
- join()
https://kotlinworld.com/141
https://velog.io/@haero_kim/Kotlin-Coroutine-Dispatchers-1%ED%8E%B8
- Coroutine's Dispatcher
Dispatch = '보내다'
스레드(Thread)에 코루틴(Coroutine)을 보낸다.
코루틴에서는 스레드 풀을 만들고, Dispatcher를 통해서 코루틴을 배분
->
코루틴을 만든 다음 해당 코루틴을 Dispatcher에 전송하면 Dispatcher는
자신이 관리하는 스레드풀 내의 스레드의 부하 상황에 맞춰 코루틴을 배분
->
해당 코루틴이 어떤 스레드 위에서 실행되게 할지 명시
코루틴의 실행을 특정 스레드에 국한 시켜주거나, 특정 스레드 풀로 전달해주는 역할을 함.
안드로이드에서는 Dispatcher가 이미 생성되어 있다.
- Dispatchers.Main : 안드로이드 메인 스레드에서 코루틴을 실행하는 디스패처
UI와 상호작용하는 작업을 실행할 때만 사용한다.
- Dispatchers.IO : 디스크 or 네트워크 I/O 작업을 실행하는데 최적화되어 있는 디스패처
- Dispatchers.Default : CPU를 많이 사용하는 작업을 기본 스레드 외부에서 실행하도록 최적화되어 있는 디스패처