[Kotlin]Coroutine

유민국·2023년 7월 23일
0

유튜브 강의

Coroutine

  • Coroutine 은 Co + Routine 즉 협력한다는 뜻의 Co (Cooperate) 와 루틴 (작업 실행 단위)의 합성어 이다.
  • Coroutine 은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴이다.
  • 코루틴은 이전에 자신의 실행이 마지막으로 중단 되었던 지점 다음의 장소에서 실행을 재개한다.

Thread vs Coroutine

  • 쓰레드는 각 태스크에 해당하는 스택 메모리를 할당받는다. 그리고 여러 작업을 동시에 수행해야할 때 OS 는 어떤 쓰레드 작업을 먼저 수행할지, 어떤 쓰레드를 더 많이 수행해야 효율적인지에 대한 스케쥴링 (선점 스케쥴링, Preempting Scheduling) 을 해야 한다.

  • Coroutuine 은 경량화 Thread 라고 부른다. 마찬가지로 작업 하나하나를 효율적으로 분배해서 동시성을 보장하는 것을 목표로 하지만, 작업 하나하나에 Thread 를 할당하는 것이 아닌 'Object' 를 할당해주고, 이 Object 를 자유롭게 스위칭함으로써 Context Switching 비용을 대폭 줄인 것이다.

Thread

  • 작업 하나하나의 단위를 Thread라 하며 각 Thread는 독립적인 Stack 메모리 영역 가지고 있다.
  • Context Switching 과정을 통해 동시성 보장한다.
  • 블로킹 (Blocking) : Thread A 가 Thread B 의 결과가 나오기까지 기다려야 한다면, Thread A 은 블로킹되어 Thread B 의 결과가 나올 때 까지 해당 자원을 쓰지 못하게 된다.

Context Switch

  • CPU의 레지스터에서 일어나는 과정으로 Task 가 바뀔 때마다 Stack (자체 메모리 영역) 과 TCB(Task Control Bloack) 를 복사, 복구하는 일을 뜻 한다.

1]
Thread A 에서 Task 1 을 수행하다가 Task 2 의 결과가 필요할 때, 비동기적으로 Thread B 를 호출을 하게 된다. 이 때 Thread A 는 블로킹되고, Thread B 로 Context Switching 이 일어나 Task 2 를 수행한다. Task 2 가 완료되면, 다시 Thread A 로 Context Switching 이 일어나 결과값을 Task 1 에 반환한다.

2]
쓰레드는 동시에 같이 수행할 Task 3, Task 4 는 각각 Thread C 와 D 에 할당되고, 선점 스케줄링(Preempting Scheduling)을 통하여 각 태스크를 알맞게 처리 하여 동시성을 보장하도록 한다.

  • Task 가 바뀔 때마다 Stack (자체 메모리 영역) 과 TCB 를 복사, 복구하는 일을 매번 해야 하는데, 일반적으로 스레드 환경에서는 스레드가 실제로 유용한 작업을 수행하는 시간을 크게 줄이기 위해 실제로 이러한 스레드를 스케줄링 하는데 낭비되는 오버헤드의 양이 최대 30~50개 라고 한다.
  • 각 스레드는 자체 스택을 할당해야 하기 때문에 메모리 사용량이 스레드 수에 따라 선형적으로 증가한다.

Coroutine 은 이와 같은 Context Switching 과정이 없다.
따라서 매우 적은 오버헤드로 매우 높은 수준의 동시성을 제공할 수 있다.

Coroutine

작업을 Coroutine Object에 할당하여 처리하는데, 이때 Context Switching 과정이 아닌, Programmer Switching (OS에서 관여하는게 아닌 코드를 통해 switch 시점을 정할 수 있다.)을 사용하여 동시성을 보장한다.

Suspend (Non-Blocking) : Object 1 이 Object 2 의 결과가 나오기까지 기다려야 한다면, Object 1 은 Suspend 되지만, Object 1 을 수행하던 Thread 는 그대로 유효하기 때문에 Object 2 도 Object 1 과 동일한 Thread 에서 실행될 수 있음

같은 프로세스 내의 Heap 에 대한 Locking 걱정 또한 사라짐

작업 단위는 Coroutine Object 이므로, Task 1 을 수행하다가 비동기 작업 Task 2 가 발생하더라도, Context Switching 없이 같은 Thread 에서 Task 2 를 수행할 수 있고(작업의 단위를 Object 로 축소하면서 하나의 Thread 가 다수의 코루틴을 다룰 수 있다)

그림 오른쪽을 보면 Thread A 와 C 가 동시에 수행되는 모습이다. 이와 같은 경우에는 동시성 보장을 위해서 Context Switching 이 필요하기 때문에 단일 Thread 에서 여러 Coroutine Object 를 컨트롤하는 것을 권장한다.

구글에서 추천하는 코루틴 사용 법

  • 코루틴은 협력작업, 예외, 이벤트 루프, 반복자, 무한 목록 및 파이프와 같은 친숙한 프로그램 구성 요소를 구현하는 데 적합하지만 이렇게 말하면 어느 타이밍에 사용할지 모르니 아래 예시를 보도록 하자

  • 비동기(콜백, 캔슬, 리소스 관리 등)로 실행하는 코드를 간단하게 만들어 준다

  • main thread가 blocking 되는 부분에서 도움을 줄 수 있다.

  • 콜백지옥으로 되어있는 코드들을 순차적으로 바꿔줄 수 있다.

  • 비동기 코드를 순차적으로 만들 수 있다.

예시 The Dream Code
val user = fetchUserData()
textView.text = user.name

--> fetchUserData에서 네트워크 콜을 발생시키기 때문에 NetworkOnMainThreadException 이 발생
-> 이 문제를 해결하기 위해 Thread를 사용
thread{
val user = fetchUserData()
textView.text = user.name
// Ui를 UI thread에서 업데이트 해야하는데 별도의 스레드에서 업데이트를 진행하니
CalledFromWrongThreadException이 발생
}
-> 결국 위와 같이 만들기 위해서는 fetchUserData라는 함수를 만들어서 Thread를 이용해서 서버를 콜하고 다시 Main Thread로 콜백을 해서 스위치를 준 다음에 UI를 업데이트하는 작업을 해야한다.
--> 니는 결국 콜백지옥을 만들게 되고 메모리 관리도 힘들게 만든다. 결국 메모리 관리를 위해서는 많은 cancel 처리를 해줘야하기도 한다.

이 부분을 코루틴으로 해결할 수 있게 한다.
코루틴은 비동기처리를 간단하게 해주고 콜백을 대체할 수 있다.

16년도에는 코루틴은 아직 실험적이었고, 18년 10 29일 날 안정화된 버전에 배포가 된다.
20년 부터 android에서 코루틴을 권장 하고 있다.

  • 코루틴의 기본적인 사용 방법

basic 1

// 이 부분이 코루틴
// launch : 코루틴 빌더
// 내부적으로 코루틴을 만들어서 반환 해준다.
// launch 를 실행하기 위해서는 GlobalScope가 필요하다
//
// GlobalScope : 코루틴 스코프
GlobalScope.launch{
    delay(1000L)
    println("World!")
}
// Thread를 사용해도 위의 코드와 동일하게 작용
// 본질적으로 코로틴은 경량 스레드라고 생각하면 된다.

thread {
    Thread.sleep(1000L)
    println("World!")
}
println("Hello, ")

basic 2

// delay : suspend fun : 일시 중단 함수
GlobalScope.launch{
    delay(1000L)
    println("World!")
}
// delay는 스코프 함수나 suspend fun 함수에서 실행될 수 있다.
// runBlocking : thread를 blocking을 할 수 있는 코루틴 함수
println("Hello, ")

// Thread.sleep(2000L) 코드를 코루틴 코드로 변경
runBlocking{
    delay(2000L)
}

basic 3

// 위 코드를 더 간결하게 하용
// main안의 코드가 끝까지 전까지는 리턴하지 않도록 만듦
fun main() = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    // 만약 위의 코드의 delay가 3000 이라면 3초가 지나기전에 코드가 끝나버린다.
    println("Hello, ")
    delay(2000L)
}

basic 4

fun main() = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello, ")
    // job이 완료될 때 까지 기다리게 된다.(job의 코루틴이 완료될때까지 기다렸다가 main함수를 종료시킨다)
    job.join()
}

basic 5
Stuctured concurrency

fun main() = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    val job2 = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello, ")
    job.join()
    job2.join()
    // 위와 같은 형태로 동시 작업을 하기 위해서는
    // job을 계속 join해줘서 관리를 해줘야한다
    // 이런 이유는 job을 GlobalScope를 사용해서 만들어 줬기 때문인데,
    // 이런식으로 하지 말고, 상위에 있는 runBlocking과 launch와의 관계를 만들어주면
    // join을 따로 하지 않아도 job이 다 끝날때 까지 기다리게 된다.
    // - 방법 : runBlocking에서 들어온 scope를 사용해서 launch 한다.

	// 부모 코루틴이 자식 코루틴이 완료될 때 까지 기다려주기 때문에 구조인 형태를 이용해서 코	루틴을 관리한다.
    val job = this.launch {
        delay(1000L)
        println("World!")
    }
    // this 를 사용하지 않아도 됨
    val job2 = launch {
        delay(1000L)
        println("World!")
    }
    println("Hello, ")
}

basic 6

// suspend 함수 만들기
fun main() = runBlocking {
    launch {
        myWorld()
    }
    println("Hello, ")
}

//fun myWorld(){
//    // 예러 이유 : 코루틴도 아니고 suspend 함수도 아닌데 호출했기 때문
//    delay(1000L)
//    println("World!")
//}

suspend fun myWorld(){
    delay(1000L)
    println("World!")
}

basic 7

// 코루틴의 경량화 정도를 보는 코드
fun main() = runBlocking {
    repeat(100_000) {
        launch {
            delay(1000L)
            print(".")
        }
    }
    // 아래 코드와 비교하면 얼마나 코루틴이 가벼운지 알 수 있다.
    // out-of-memory error가 발생할 수 있음.(반복수가 더 커질 경우에)
//    repeat(100_000) {
//        thread {
//            Thread.sleep(1000L)
//            print(".")
//        }
//    }
}

basic 8

// 프로세스가 끝나면 코루틴도 끝남
fun main() = runBlocking {
    GlobalScope.launch {
        repeat(100) {i ->
            // 0.5초 마다 print를 하지만 프로세스가 종료되면 코루틴은 종료 된다.
            print("I'm sleeping $i ...")
            delay(500L)
        }
    }

    // 1.3초 후에 프로세스가 종료
    delay(1300L)
}

basic 9

// 코루틴의 일시중지, 재개를 살펴보자

// delay가 따로 없는 경우
//fun main() = runBlocking {
//    launch{
//        repeat(5){i ->
//            println("Coroutine A, $i")
//        }
//    }
//
//    launch{
//        repeat(5){i ->
//            println("Coroutine B, $i")
//        }
//    }
//
//    println("Coroutine Outer")
//}

// A 코루틴에 delay
//fun main() = runBlocking {
//    launch{
//        repeat(5){i ->
//            println("Coroutine A, $i")
//            delay(10L)
//        }
//    }
//
//    launch{
//        repeat(5){i ->
//            println("Coroutine B, $i")
//        }
//    }
//
//    println("Coroutine Outer")
//}

// A,B 번갈아가면서 delay 줘보기
fun main() = runBlocking {
    launch{
        repeat(5){i ->
            println("Coroutine A, $i")
            delay(10L)
        }
    }

    launch{
        repeat(5){i ->
            println("Coroutine B, $i")
            delay(10L)
        }
    }

    println("Coroutine Outer")
}

// 어느 스레드에서 출력되는지 알기 위해 println을 overriding 함
// -Dkotlinx.coroutines.debug 디버그 옵션을 넣어서 어떤 코루틴에서 실행되는지 알 수 있음.
fun <T>println(msg:T){
    kotlin.io.println("$msg [${Thread.currentThread().name}]")
}

정리
Coroutine builder : 코루틴을 실행시키기 위해 사용

  • launch : 코루틴이 실행하고 job이라는 반환객체도 만들어봤음.
  • runBlocking :
    Scope : builder들은 scope에서 실행되었음
  • CoroutineScope
  • GlobalScope
    Suspend function : 일시중단 함수
  • suspend
  • delay()
  • join()
    Structured concurrency : join등을 사용해서 관리하지 않아도 코드가 실행될 수 있게 관리 됨

코루틴 Cancel

  1. job.cancel()/job.cancelAndJoin()을 사용한다
  2. Cancellation is cooperative
    way 1 : suspend fun 호출 -> 예외를 던지는 방식
    1] yield()를 사용하면 delay(1L)처럼 사용하지 않고도 suspend fun를 호출하여 캔슬 시킬 수 있다.
    way 2 : isActive를 체크해서 cancel해준다 -> 예외를 던지지 않음
  3. Timeout
    1] withTimeout
    2] withTimeoutOrNull
  4. 리소스 해제
    finally 블록에서 실행 중인 코루틴의 리소스를 해제한다.

Suspending function

anync, await를 통해서 비동기, 동기 조절 가능
CoroutineStart.LAZY를 통해 지연 시킬 수 있고
start()함수를 실행시켜 지연된 suspend fun을 실행 시킬 수 있다.

Coroutine Context and Dispatchers

  1. Dispatchers 와 Threads의 관계
    Dispatchers를 주지 않은 경우 runBlocking에 해당하는 thread에서 실행하게 된다.
    Dispatchers.Unconfined :
    Dispatchers.Default : GlobalScope
    Dispatchers.IO
    newSingleContext

부모 코루틴의 책임
자식 코루틴이 끝날 때 까지 기다려준다.(join을 사용하지 않아도 된다)

profile
안녕하세요 😊

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

좋은 글 감사합니다.

답글 달기