android
15. 안드로이드 동시성 코루틴 공부
코코넛딩
2023. 12. 14. 14:28
이 글은 챗지피티와 아래의 블로그들을 참고하였습니다.
Kotlin, 코루틴 제대로 이해하기 - (1)
kotlin의 Coroutine을 이해하는 것이 해당 포스팅의 목표입니다. 🔗 Kotlin 시리즈 모아보기 사실, 순서대로라면 Class에 대한 내용을 다뤄야하는데, 추석이 끝나고 마음이 급해져서 코루틴이라도 파보
gngsn.tistory.com
코루틴이 뭘까요?
- 코루틴은 경량 스레드와 비슷한 실행단위로, 비동기처리가 가능하다.
- 장점
- 코드의 가독성 유지
- 코루틴은 스레드 보다 가볍다.
- 멈추거나 재개할 수 있다.
코루틴컨텍스트와 코루틴빌더에 대한 설명
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
/*
출력 결과
Hello
World!
*/
- 위 코드의 설명
- Hello 다음에 World!로 나오는 이유는 delay 함수에서 코루틴을 1초 동안 중단할 때 다른 코루틴 또는 메인쓰레드의 작업은 계속 진행되기 때문이다.
//예제 1번
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
launch { // 부모의 context. main runblocking context입니다.
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // not confined -- 메인 스레드에서 동작합니다.
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // DefaultDispatcher로 작업이 보내집니다.
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 새로운 스레드로 작업이 전달됩니다.
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
//예제 2번
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
launch(Dispatchers.Unconfined) {
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
launch {
println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
delay(1000)
println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}
}
/*
Unconfined : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main
*/
- 주의 !
- 예제는 메인스레드가 사용되었습니다.
- 메인 스레드에서 runBlocking{}을 수행하면 메인 쓰레드가 정지되어 ANR이 발생할 수 있으므로 메인 쓰레드에서는 사용하지 않는 것이 좋다.
- launch {}
- launch{}를 매개 변수 없이 실행 하는 경우 실행 중인 CouroutineScope에서 dispatcher와 함께 context를 상속 받는다.
- 이 예제의 경우 메인 스레드에서 실행되는 main의 runBlocking 코루틴의 context를 받습니다.
- Dispatchers.Unconfined
- 메인쓰레드(runBlocking{}의 영향을 받음)에서 실행되는 것 처럼 보이는 특수한 dispatcher 같지만 사실은 다른 메커니즘이다.
- Unconfined dispatcher VS Confined dispatcher
- Dispatchers.Unconfined
- 이 dispatcher는 호출한 쓰레드에서 코루틴을 시작하지만
- 첫번째 suspension 지점(코루틴이 멈추는 시점, delay())까지만 시작합니다.
- suspension이 끝나면 호출된 suspension 함수에 의해 다른 스레드로 코루틴을 재개합니다.
- CPU의 시간을 소비하지 않고 특정 스레드에 제한된 공유 데이터(UI)를 업데이트 하지 않는 코루틴에는 unconfined dispatcher가 적합
- 코루틴의 일부 연산을 즉시 수행해야해서 나중에 실행할 필요가 없거나(delay를 사용하지 않을 때를 말하는 듯)
- side effect가 발생하는 특정 사례에서 도움이 될 수 있다.
- unconfined dispatcher는 일반 코드에서 사용해서는 안된다.
- confined dispatcher
- 기본적으로 외부 CoroutineScope로 부터 dispatcher를 상속받는다.
- 위의 예제 2번 참고
- launch {}는 실행중인 코루틴 스코프의 dispatcher의 스레드에 제한되므로 이를 상속받으면 예측 가능한 FIFO 스케줄링을 통해 이 스레드의 실행을 제한할 수 있는 효과가 있다.
- 같은 스레드에서 실행되는 코루틴은 순차적으로(FIFO) 실행된다.
- launch {}는 실행중인 코루틴 스코프의 dispatcher의 스레드에 제한되므로 이를 상속받으면 예측 가능한 FIFO 스케줄링을 통해 이 스레드의 실행을 제한할 수 있는 효과가 있다.
- 예제 로그 분석
- launch
- runBlocking의 dispatcher는 호출한 스레드에 제한된다.
- runBlocking에서 context를 상속 받은 코루틴은 main 스레드에서 계속 실행된다.
- unconfined dispatcher
- delay 함수가 사용 중인 기본 excutor 스레드에서 다시 실행된다.
- launch
- Dispatchers.Unconfined
- Dispatcher.Default
- GlobalScope에서 코루틴이 실행될 때 dispatcher는 기본적으로 Dispatcher.Default 혹은 백그라운드 스레드 풀을 공유하여 사용합니다. 그러므도 lauch(Dispatcer.Default) {} 와 GlobalScope.launch {} 는 같은 dispatcher 입니다.
- 메인 스레드와 별개로 동작하며, 백그라운드 쓰레드에서 작업이 실행된다.
- 코루틴은 Dispatcher.Default를 사용할 때, 공유된 백그라운드 스레드 풀에서 스레드를 가져온다.
- CPU 집약적 작업에 최적화 되어있음
- CPU 집약적 작업
- CPU가 대부분 연산을 수행하며, 입출력(I/O)작업은 거의 없는 경우
- 계산 중심적
- 복잡한 계산, 알고리즘, 데이터 처리
- 행렬 연산, 암호화, 머신러닝 모델 학습
- CPU 집약적 작업
- Dispatcher.IO
- I/O(파일, 네트워크) 작업에 최적화된 Dispatcher
- 기본적으로 Default 스레드 풀 공유
- newSingleThreadContext
- newSingleThreadContext는 코루틴을 실행할 새로운 스레드를 생성합니다.
- 이 스레드는 매우 비싼 자원입니다.
- 앱에서는 더이상 필요하지 않을 때 close 함수를 사용하여 해제하거나 최상위 변수에 저장한 후 애플리케이션 전체에 걸쳐 사용해야한다.
- 코루틴 컨텍스트(CoroutineContext)
- 컨텍스트를 명시적으로 지정하지 않으면 기본적으로 부모 코루틴의 컨텍스트를 상속받아 실행된다.
- 코루틴 컨텍스트에 포함되는 요소
- Dispatchers
- 코루틴이 실행될 쓰레드 또는 쓰레드 풀을 결정합니다.
- Dispatcher.Main
- 메인 쓰레드에서 실행
- Dispatcher.IO
- I/O 작업을 위한 백그라운드 스레드에서 실행
- Dispatcher.Default
- CPU 집약적 작업을 위한 백그라운드 스레드에서 실행
- Dispatcher.Main
- 코루틴이 실행될 쓰레드 또는 쓰레드 풀을 결정합니다.
- Job
- 코루틴 생명주기를 관리합니다.
- Job은 코루틴의 시작, 취소, 완료 등의 상태를 나타낸다.
- 아래에 자세한 설명 있습니다.
- CoroutineExceptionHandler
- 코루틴 내에서 발생하는 예외를 처리하는데 사용된다.
- Dispatchers
- launch{} 코루틴 빌더
- Job 반환
- launch 블록은 Job 객체를 반환한다.
- 이 객체를 사용하여 코루틴의 생명 주기를 관리할 수 있다.
- 컨텍스트 지정
- launch 블록은 코루틴 컨텍스트를 지정할 수 있으며 이를 통해 코루틴이 실행될 스레드를 결정할 수 있다.
- 주요 사항
- launch 블록 안에서 실행되는 코드는 비동기적이므로, 블록 바깥의 코드는 launch 블록의 완료를 기다리지 않고 즉시 실행된다.
- launch에 전달된 람다 표현식 내부에서는 코루틴 관련 함수를 사용할 수 있습니다. (예: delay)
- 코루틴의 완료를 기다리기 위해 Job.join() 메서드를 사용할 수 있습니다. 이는 해당 코루틴이 완료될 때 까지 현재 스레드의 실행을 중단합니다.
- CoroutineContext를 launch에 전달하여 코루틴의 동작 방식을 조정할 수 있습니다. 예를 들어 Dispatchers.Main은 메인 스레드에서 코루틴을 실행하도록 지정합니다.
- Job 반환
- delay()
- 일시정지 기능
- 특정 시간 동안 코루틴을 일시정지 시킨다.
- 이 일시정지 동안 다른 코루틴이 실행될 수 있다.
- 현재 쓰레드를 차단하지 않는다.
- runBlocking{}
- 코루틴에서 사용되는 블록
- 코루틴을 직접 생성하지는 않지만 코루틴 스코프를 생성하여 그 안에서 코루틴을 실행할 수 있는 환경을 제공한다.
- 새로운 코루틴을 시작하고 해당 블록 내의 모든 코루틴이 완료될 때까지 현재 스레드를 차단한다.
- runBlocking의 dispatcher는 호출한 스레드에 제한된다.
- 주로 테스트나 메인함수에서 코루틴 코드를 동기적으로 실행하기 위해서 사용한다.
- runBlocking은 코루틴이 완료될 때까지 프로그램의 실행을 중지 시키기 때문에 일반적인 앱에서는 사용을 자제하는 것이 좋다.
- async
- 코루틴 빌더 중 하나로 비동기 작업을 시작하고 결과를 나중에 받을 수 있게 해주는 함수이다.
- async는 Deferred 객체를 반환한다. 이 Deferred 객체를 통해 비동기 작업의 결과를 나중에 가져올 수 있다.
- 특징
- 비동기 실행
- async를 사용하여 시작된 코루틴은 호출자 스레드를 차단하지 않고 백그라운드에서 비동기적으로 실행된다.
- 결과 반환
- async는 Deferred 객체를 반환한다.
- 이 객체의 await()메서드를 호출하여 코루틴이 완료될 때까지 기다린 후 결과를 받을 수 있습니다.
- 값 반환
- launch와 달리 async는 결과값을 반환할 수 있습니다.
- 코드 블록의 마지막 표현식이 Deferred 객체에 의해 반환됩니다.
- 예외 처리
- async에서 발생한 예외는 Deferred 객체에 저장되며, await() 호출시에 예외가 발생합니다.
- 비동기 실행
//async관련 코드
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
// 비동기로 실행할 작업
delay(1000L) // 비동기 대기
return@async 42 // 결과값 반환
}
println("작업 중...")
val result: Int = deferred.await() // 작업 완료를 기다리고 결과를 받음
println("결과: $result") //await()가 끝나야 이 코드가 실행됨
}
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 비동기 작업 시작
lifecycleScope.launch {
val result = async(Dispatchers.IO) {
// 네트워크 호출 또는 파일 읽기
fetchDataFromNetwork()
}
// 결과 기다림
val data = result.await()
// UI 업데이트 (메인 스레드)
updateUI(data)
}
}
private suspend fun fetchDataFromNetwork(): String {
delay(1000L) // 네트워크 지연 시뮬레이션
return "Hello, World!"
}
private fun updateUI(data: String) {
// UI 업데이트
findViewById<TextView>(R.id.textView).text = data
}
}
- async{}가 완료되어야 await() 뒤에 코드를 실행할 수 있다.
- lifecycleScope.launch {}로 async를 감싼다.
Structured Concurrency
import kotlinx.coroutines.*
fun main() = runBlocking { // 상위 코루틴 스코프
launch { // 자식 코루틴
// 여기에 작업을 정의합니다.
}
// runBlocking 스코프는 내부의 모든 코루틴이 완료될 때까지 대기합니다.
}
- 위의 코드에 대한 설명
- runBlocking은 상위 코루틴 스코프를 제공하며, 이 스코프 내에서 시작된 모든 코루틴은 runBlocking 스코프에 의해 관리된다.
- 주요 특징
- 스코프에 의한 관리
- 모든 코루틴은 특정 스코프 내에서 실행됩니다.
- 이 스코프는 코루틴의 실행, 취소, 완료를 관리합니다.
- 부모-자식 관계
- 코루틴 스코프 내에서 시작된 모든 코루틴은 부모 코루틴과 연결됩니다.
- 부모 코루틴이 종료되면 자식 코루틴도 함께 종료됩니다.
- 오류 전파
- 부모 코루틴 또는 스코프 내에서 발생한 오류는 자식 코루틴으로 전파된다.
- 생명주기 관리
- 코루틴 스코프는 해당 스코프 내의 모든 코루틴이 완료될 때까지 종료되지 않습니다.
- 이로 인해 코루틴의 실행이 잘 관리되며, 누락되거나 고아화된 코루틴을 방지합니다.
- 스코프에 의한 관리
suspend
- suspend 키워드는 suspend 함수가 코루틴의 실행을 중단(suspend)하고 suspend 함수가 완료되면 코루틴을 재개할 수 있음을 나타낸다.
- 이러한 suspend 함수는 코루틴 내에서만 호출될 수 있으며, 비동기 suspend 작업을 수행하는 동안 현재 코루틴을 중단시키고 해당 작업이 완료될때까지 기다린후 다시 재개합니다.
- suspend 함수의 특징
- 비동기 작업 수행
- suspend 함수는 네트워크 요청, 데이터베이스 접근과 같은 비동기 작업을 쉽게 처리할 수 있도록 도와준다.
- 코루틴의 중단 및 재개
- suspend함수는 현재 코루틴의 실행을 중단 시킬 수 있으며, suspend가 완료된 후 코루틴을 재개한다.
- 스레드 차단 방지
- 일반적인 비동기 작업과 달리 suspend 함수는 스레드를 차단하지 않고 비동기 작업을 수행한다.
- 비동기 작업 수행
import kotlinx.coroutines.*
suspend fun fetchData(): String {
// 비동기로 데이터를 가져오는 작업
delay(1000) // 비동기 대기를 시뮬레이션
return "데이터"
}
fun main() = runBlocking {
val data = fetchData() // suspend 함수 호출
println(data)
}
Coroutine Scope
import kotlinx.coroutines.*
fun main() {
println("Main Scope Started")
CoroutineScope(Dispatchers.Default).launch { // 상위 CoroutineScope에서 launch
println("Task 1 started in main scope")
// 하위 coroutineScope 생성
coroutineScope {
println("Entered inner coroutineScope")
// 하위 coroutineScope 안에서 launch
launch {
delay(1000)
println("Task 1 in inner coroutineScope completed")
}
launch {
delay(500)
println("Task 2 in inner coroutineScope completed")
}
}
println("Exited inner coroutineScope, back to main scope")
}
Thread.sleep(2000) // 메인 스레드가 종료되지 않도록 대기
println("Main Scope Completed")
}
- 코루틴 스코프는 코루틴이 실행되고 관리되는 환경을 제공한다.
- 환경
- 코루틴의 수명주기, 실행 컨텍스트(Dispatcher), 예외 처리 등을 관리한다.
- 코루틴 스코프의 주요 역할
- 코루틴 관리
- 코루틴 스코프 안에서 실행된 코루틴은 스코프가 종료될 때 함께 정리 된다.
- 컨택스트 제공
- Dispatcher롸 같은 컨택스트를 코루틴에 제공해 실행을 제어한다.
- 구조적 동시성
- 자식 코루틴 들이 스코프를 벗어나지 못하도록 제한하여 안정적으로 작업을 완료한다.
- 코루틴 관리
- 환경
- 코루틴 스코프의 생성 방법
- runBlocking
- main함수(onCreate())에서 자주 사용됨
- 현재 스레드를 차단하면서 코루틴 환경을 생성하기 때문에, 프로그램 진입점(main(), onCreate())에서 코루틴을 동기적으로 실행하기에 편리하기 때문이다.
- main함수(onCreate())에서 자주 사용됨
- CoroutineScope
- 명시적으로 스코프를 생성한다.
- coroutineScope
- 일시 중단 함수로, 기존 코루틴 스코프 내에서 하위 스코프를 만든다.
- runBlocking
- 코루틴 빌더
- 코루틴 스코프 안에서 코루틴 빌더를 사용하여 여러 코루틴을 실행할 수 있다.
- launch
- async
- runBlocking
coroutineScope
import kotlinx.coroutines.*
fun main() = runBlocking {
coroutineScope { // 새로운 코루틴 스코프 생성
launch { //(1번)
delay(1000L)
println("first 1000")
}
coroutineScope { //(2번)
launch { //(2-1번)
UtilFunction.log("no delay")
}
launch { //(2-2번)
delay(1000L)
UtilFunction.log("delay 1000")
}
}
delay(500L) //(3번)
println("Task from coroutine scope")
}
println("Coroutine scope is over") //(4번)
}
/*
no delay
first 1000
delay 1000
Task from coroutine scope
Coroutine scope is over
*/
- 특징
- coroutineScope{}는 비동기적이다. coroutineScope{}안의 코루틴들은 동시에 시작된다.
- 1번과 2번이 동시에 시작된다.
- 1번이 2번의 coroutineScope 보다 먼저임으로 2번의 coroutineScope로 1번을 멈출수 없다.
- 3번은 2번이 끝난 후에 시작된다.
- 3번은 coroutineScope의 뒤에 있기때문에 일시정지가 되었다.
- 1번과 2번이 동시에 시작된다.
- coroutineScope는 일시중단함수(suspend function)이다.
- 일시 중단 함수는 반드시 코루틴 컨텍스트가 있어야 호출가능하며, 일반 함수처럼 호출할 수 없다.
- 코루틴 스코프 생성
- coroutineScope는 상위 코루틴 컨텍스트를 기반으로 새로운 코루틴 스코프를 생성한다.
- 즉 코루틴 안에서만 호출 가능한 하위 스코프 생성 도구이다.
- 자식 코루틴 관리
- coroutineScope가 생성한 새로운 스코프 내에서 시작된 모든 자식 코루틴이 완료될 때까지 현재 코루틴의 실행을 일시 중지합니다.
- 오류 전파
- 스코프 내의 어떤 코루틴에서 오류가 발생하면, 해당 스코프 내의 모든 코루틴이 취소됩니다.
- coroutineScope{}는 비동기적이다. coroutineScope{}안의 코루틴들은 동시에 시작된다.
- 주의 사항
- coroutineScope는 호출된 코루틴을 일시중지 시킬수 있으므로 이를 사용할 때는 현재 코루틴의 실행 흐름을 고려해야합니다.
- runBlocking과 다른 점
- runBlocking은 주로 테스트나 메인함수에서 사용되며 현재 스레드를 차단한다.
- coroutineScope는 현재 코루틴을 일시 중지 시키고 새로운 코루틴 스코프를 생성한다.
Job
1. Job의 상태
코루틴의 Job은 다음과 같은 6가지 상태를 갖는다.
이 상태를 바탕으로 제어할 수 있다.
isActive, isCompleted, isCancelled는 다음과 같이 확인 가능
val job = launch{
//coroutine
}
job.isActive // isActive(Boolean)반환
job.isCancelled // isCancelled(Boolean)반환
job.isCompleted // isCompleted(Boolean)반환
2. Job의 메소드
1. start()
fun main() {
val job = CoroutineScope(Dispatchers.IO).launch(start = CoroutineStart.LAZY) {
print("print!!")
}
job.start()
}
- job을 실행한다.
- start()의 반환값은 Boolean이다.
- 호출했을 때 코루틴이 동작 중이면 true
- 준비, 완료상태이면 false
- start로 생성된 job은 중단(suspend)없이 실행된다. 즉, CouroutineScope 안에서 실행하지 않아도 됨
- start를 호출한 스레드가 종료되면 job도 같이종료됨
- 위 코드는 job.start() 함수를 호출한 MainThread가 종료되면 IO스레드도 같이 종료되기 때문에 "print!!"가 출력되지 않는다.
2. join()
suspend fun main() {
val job = CoroutineScope(Dispatchers.IO).launch(start = CoroutineStart.LAZY) {
print("print!!")
}
job.join()
}
- job을 실행한다.
- Job의 동작이 완료될 때 까지 join을 호출한 코루틴을 일시중단합니다.
- MainThread가 job이 끝날 때 까지 일시중단되기 때문에 "print!!"가 출력된다.
3. canel()
- 현재 코루틴에 종료를 유도하고 start()와 마찬가지로 대기하지 않는다.
- CouroutineScope 밖에서 호출해도 무관
- 만약 타이트하게 동작하는 반복문에 delay가 없다면 종료하지 못한다.
4. cancelAndJoin()
- 현재 코루틴을 즉시 종료하라는 신호를 보낸 후 정상 종료될 때 까지 대기한다.
5. cancelChildren()
- CoroutineScope내에 작성한 자식 코루틴들을 종료한다.
- cancel()과 다르게 하위아이템만 종료하며, 상위 코루틴은 취소하지 않습니다.
3. Job 활용하기
val job = CoroutineScope(Dispatcher.IO).launch{
// coroutine
}
- Job은 위와 같이 1개의 launch를 관리할 수 있다.
val job = Job() // 하나의 Job 생성
- 여러 Job을 관리하려면 하나의 Job을 생성하고 N개의 laucher와 actor를 관리하면된다.
CoroutineScope(Dispatchers.Default + job)
- CoroutineScope에 job을 함께 초기화하면 CoroutineScope의 자식까지 모두 관리할 수 있습니다.
.launch(Dispatchers.IO + job)
- launch에만 적용할 수 있습니다.
디버깅 코루틴과 스레드
import kotlinx.coroutines.*
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main() = runBlocking<Unit> {
val a = async {
log("I'm computing a piece of the answer")
6
}
val b = async {
log("I'm computing another piece of the answer")
7
}
log("The answer is ${a.await() * b.await()}")
}
/*
[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42
*/
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
println("My job is ${coroutineContext[Job]}")
}
/*
My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
*/
- 코루틴은 한 스레드에 의해 중지되고 다른 스레드에 의해 개재 될 수 있다.
- 싱글 스레드 dispatcher로는 코루틴이 어디에 있는지 파악이 어려울 수 있다.
- 스레드를 사용해서 앱을 디버깅하는 방법은 각 로그에 스레드 이름을 출력하는 것이다.
- 이 기능은 로깅 프레임워크에 의해 지원된다.
- 코루틴을 사용할 때 스레드의 이름만으로는 context를 판단하기 어렵기 때문에 kotlinx.coroutines에서는 디버깅에 대한 지원이 제공된다.
- Job은 context의 일부로서 coroutineContext[Job] 식을 사용하여 검색할 수 있다.
- 위의 예제를 터미널이나 gradle에서 -Dkotlinx.coroutines.debug 옵션을 사용해 실행해보자