Strong 감자의 공부

Ch_14 코루틴 처리 본문

문법_Kotlin

Ch_14 코루틴 처리

ugyeong 2023. 11. 12. 10:49

- 목차 

1. 코루틴 동시성 알아보기 

2. 코루틴 정보 전달 알아보기


 

🌷코루틴의 기본개념 

- 코루틴의 주요 특징은 서브루틴(함수가 호출되면 결과를 반환, 순차적 처리)과는 다르게 실행중 중단이 가능하며 필요할 때 다시 그 지점부터 실행 재개가 가능합니다.

 

🍭non-blocking에 대해

더보기
  1. 중단 및 재개: 코루틴은 특정 작업(예: I/O 작업)을 실행할 때 중단될 수 있습니다. 이때, 코루틴은 현재 상태를 저장하고 실행을 일시 중지합니다. 이러한 기능 덕분에 코루틴이 블로킹 작업을 요청하더라도, 다른 코루틴으로 실행 흐름을 넘겨 프로그램의 나머지 부분이 계속 실행될 수 있습니다.
  2. 이벤트 루프 및 스케줄러: 코루틴은 이벤트 루프나 스케줄러와 같은 메커니즘을 사용하여 관리됩니다. 이러한 메커니즘은 코루틴 간에 실행을 효율적으로 전환하고, I/O와 같은 논블로킹 작업이 완료되었을 때 코루틴을 재개합니다.
  3. 비동기 I/O 작업: 코루틴은 비동기 I/O 작업에 최적화되어 있습니다. 예를 들어, 네트워크 요청을 할 때 코루틴은 요청을 보내고, 응답을 기다리는 동안 중단됩니다. 이때 다른 코루틴은 계속 실행될 수 있으며, 응답이 도착하면 원래 코루틴은 다시 활성화되어 나머지 작업을 수행합니다.
  4. 효율적인 자원 사용: 코루틴은 전통적인 멀티스레딩에 비해 상대 코루틴은 스레드에 비해 적은 메모리를 사용합니다. 각 스레드는 일반적으로 고정된 스택 메모리(예: 수 MB)를 할당받지만, 코루틴은 필요한 만큼의 메모리만 사용하며, 이는 대개 훨씬 적은 양입니다. 적으로 적은 오버헤드로 많은 수의 동시 작업을 처리할 수 있습니다. 또한, 컨텍스트 스위칭: 코루틴 간의 컨텍스트 스위칭은 스레드 간 스위칭보다 오버헤드가 훨씬 적습니다. 스레드 간의 컨텍스트 스위칭은 운영 체제의 개입을 필요로 하며, 이는 상대적으로 많은 시간과 자원을 소모합니다. 반면, 코루틴은 사용자 레벨에서의 경량 스위칭을 통해 빠르고 효율적으로 실행 상태를 전환합니다. (코루틴이 실행 상태를 전환할 때 필요한 데이터와 상태 정보를 최소화하여 관리한다는 의미)

- 코루틴은 스레드를 사용하지만, 스레드와 다른 점은 블로킹을 하지 않고 난블로킹 상태로 코루틴을 스레드 환경에서 실행한다. 그래서 일반적인 스레드 처리보다 메모리 성능이 뛰어납니다.

 

🌷 코루틴 구성

스레드와 달리 코루틴은 코루틴이 실행되는 별도의 영역인 코루틴 스코프를 구성해야한다. 그리고 이 코루틴 스코프에서 코루틴 빌더 함수와 일시중단 함수를 사용합니다.

- 코루틴 스코프 : GlobalScope, CoroutineScope는 코루틴 스코프를 구성해서 내부에 코루틴 빌더 함수로 코루틴을 계층 구성하고 내부에 일시 중단 함수 등을 사용해서 코루틴을 처리하는 영역입니다.

- 코루틴 빌더 함수 : launch, runBlocking 등은 코루틴 스코프를 생성하는 함수이거나 코루틴을 처리하는 함수입니다.

- 일반 중단 함수 : delay 함수는 코루틴 내부의 특정 기능을 하는 함수입니다.

- 코루틴 결과 처리 :  Job과 Deffeered는 코루틴의 결과를 반환하거나 코루틴 처리의 중단 등을 처리 할 수 있습니다. 

 

🌷 런블로킹 작동원리 

런블러킹 방식은 스레드와 코루틴의 중간정도의 요건을 처리한다.세북적인 처리 내용을 보면 코루틴을 스레드에서 간단하게 처리하는 방식을 제공합니다.

단점 : 현재 처리되는 스레드가 종료되면 같이 종료됩니다.

아래의 방식은 메인 함수는 일반 스레드가 작동하고 그 내부에 tunBlocking이 빌더한 경우만 코루틴으로 처리합니다.

 

🌷 주요 코루틴 빌더 함수

- launch 확장함수 : 코루틴 스코프에 코루틴을 빌더하는 함수이고 반환값은 Job이다. 이 Job으로 코루틴을 중단할 수 있습니다.

- async 확장함수 : 코루틴 스코프에 코루틴을 빌더하는 함수이다. 반환값은 Deffered<T>이고 이에 코루틴의 처리결과도 같이 반환된다. 그래서 await로 반환값을 처리할 수 있습니다.

- withContext 일시중단함수 : 컨텍스트를 변경하면서 처리하는 코루틴 빌더이다. 특히 예외 처리가 필요할 때, finally 내에 noncallable과 같이 처리하면 세로운 코루틴을 실행시킬 수 있습니다.

- withTimeOut 일시중단함수 :특정 코루틴 처리를 특정 시간까지 작동합니다.

 

🌷 GlobalScope, CoroutineScope차이

  • 글로벌 스코프: 앱의 생명 주기와 함께 동작하기 때문에 앱이 실행되는 동안은 별도의 생명주기 관리가 필요하지 않습니다. 만약 앱의 시작부터 종료될 때까지 혹은 장시간 실행되어야 하는 코루틴이 있다면 GlobalScope를 사용하면 됩니다.
  • 코루틴 스코프: 버튼을 클릭해서 서버의 정보를 가져오거나 파일을 여는 용도라면 필요할 때 만 열고 완료되면 닫는 CoroutineScope를 사용해야 합니다

🌷 join() 메서드는 코루틴이 실행이 종료된 후 메인스레드 코루틴이 종료합니다.

 코루틴 스코프 안에 선언된 여러 개의 launch 블록은 모두 새로운 코루틴으로 분기되면서 동시에 처리되기 때문에 순서를 정할 수 없습니다.

이럴 때 launch 블록 끝에 join() 메서드를 사용하면 각각의 코루틴이 순차적으로 실행됩니다.

CroutineScope(Dispatchers.default).launch() {
    launch {
        for (i in 0..5) {
            delay(500)
            Log.d("코루틴", "결과1 = $i")
        }
    }.join()

    launch {
        for (i in 0..5) {
            delay(500)
            Log.d("코루틴", "결과2 = $i")
        }
    }
}

🌷 코루틴은 launch와 async로 시작할 수 있습니다.

- launch() : 호출하는 것만으로도 코루틴을 생성, 반환되는 Job을 변수에 넣어두고 상태관리용으로 사용합니다.

- async() : 상태를 관리하고 await를 이용해 반환값을 받을 수 있습니다. 

CoroutineScope(Dispatchers.Default).async {
    val deferred1 = async {
        delay(500)
        350
    }
    val deferred2 = async {
        delay(1000)
        200
    }
    Log.d("코루틴", "연산 결과 = ${deferred1.await() + deferred2.await()}")
}

🌷cancel() : 코루틴의 동작을 멈추는 상태 관리 메서드입니다.

하나의 스코프 안에 여러 개의 코루틴이 있다면 하위의 코루틴도 모두 동작을 멈춥니다.

다음 코드의 job의 cancel메서드가 호출되면 job 뿐만 아니라 같은 스코프에 있는 job1의 코드도 모두 동작을 중단합니다. job의 cancel메서드가 호출되면 job 뿐만 아니라 같은 스코프에 있는 job1의 코드도 모두 동작을 중단합니다.

val job = CoroutineScope(Dispatchers.Default).launch {
    var job1 = launch {
        for (i in 0..10) {
            delay(500)
            Log.d("코루틴", "결과 = $i")
        }
    }
}

🌷suspend

코루틴 안에서 suspend 키워드로 선언된 함수가 호출되면 이전까지의 코드 실행이 멈추고, suspend 함수가 처리가 완료된 후에 멈춰 있던 원래 스코프의 다음 코드가 실행됩니다.

 

- subRoutine() 함수를 suspend 키워드로 선언합니다.

CoroutineScope가 실행되면 (코드 1)이라고 작성된 부분이 실행된 후 subRoune() 함수가 호출됩니다.

그리고 suspend 키워드를 사용했기 때문에 subRoutine() 안의 코드가 모두 실행된 후에 (코드 2)가 실행됩니다.

suspend가 코루틴을 가장 잘 나타내는 이유는 subRoutine()이 실행되면서 호출한 측의 코드를 잠시 멈췄지만 스레드의 중단이 없기 때문입니다.

- 이 코드를 스레드로 작성했다면??

부모에 해당하는 ‘(코드 1)’이 동작하는 스레드를 멈춰야만 가능한데, 코루틴에서는 부모 루틴의 상태 값을 저장한 후 subRoutine()을 실행하고, 다시 subRoutine()이 종류된 후 부모 루틴의 상태 값을 복원하는 형태로 동작하므로 스레드에는 영향을 주지 않습니다.

이런 구조가 스레드의 동시성에서 발생할 수 있는 성능 저하도 막아줍니다.

suspend fun subRoutine() {
    for (i in 0..10) {
        Log.d("subRoutine", "$i")
    }
}

CoroutineScope(Dispatchers.Main).launch {
    // (코드 1)
    subRoutine()
    // (코드 2)
}

 

🌷Dispatcher

 코루틴이 실행될 스레드를 지정하는 것이라고 생각하면 됩니다.

 

-디스패처의 종류

 

코루틴이 실행될 스레드를 정하는 디스패처 (Dispatcher) 는 IO, Main, Default, Unconfined 등이 있는데, 모두 사용할 필요는 없고 우선은 IO와 Main을 잘 조합해서 사용하면 됩니다.

Dispatchers.Default CPU를 많이 사용하는 작업을 백그라운드 스레드에서 실행하도록 최적화되어 있는 디스패처입니다. 안드로이드의 기본 스레드풀 (Thread Pool)을 사용합니다.
Dispatchers.IO 이미지 다운로드, 파일 입출력 등의 입출력에 최적화되어 있는 디스패처입니다.
Dispatchers.Main 안드로이드의 기본 스레드에서 코루틴을 실행하고 UI와 상호작용에 최적화되어 있는 디스패처입니다. 택스트뷰에 글자를 입력해야 할 경우 Main 컨텍스트를 사용해야 합니다.
Dispatchers.Unconfined 자신을 호출한 컨텍스트를 기본으로 사용하는데, 중단 후 다시 실행하는 시점에 컨텍스트가 바뀌면 자신의 컨텍스트도 다시 실행하는 컨텍스트를 따라갑니다.

 

🌷withContext로 디스패처 분리 

suspend 함수를 코루틴 스코프에서 호출할 때 호출한 스코프와 다른 디스패처를 사용할 때가 있습니다.

예를 들어 호출 측 코루틴은 main 디스패처에서 UI를 제어하는데, 호출되는 suspend함수는 디스크에서 파일을 일거와야 하는 경우가 있습니다.

이럴 때 withCotext를 사용해서 호출되는 suspend 함수의 디스패처를 IO로 변경할 수 있습니다.

호출되는 suspend 함수에 반환 값이 있다면 변숭 저장하고 사용할 수도 있습니다.

suspend fun readFile(): String {
    return "파일 내용"
}

CoroutineScope(Dispatchers.Main).launch {
    // 화면 처리
    val result = withContext(Dispatchers.IO) {
        readFile()
    }
    Log.d("코루틴", "파일 결과 = $result")
}