Kotlin/코틀린 코루틴의 정석

[Kotlin 코루틴] 코루틴의 이해

꾸준함. 2024. 11. 26. 22:45

서브루틴과 코루틴

 

1. 루틴과 서브루틴

  • 프로그래밍 과점에서 루틴은 `특정한 일을 처리하기 위한 일련의 명령`이라는 뜻으로 사용
    • 이런 일련의 명령을 함수 또는 메서드라고 지칭

 

  • 서브루틴은 함수 내에서 함수가 호출될 경우 호출된 함수
    • 간단하게 설명하면 서브루틴이란 함수의 하위(sub)에서 실행되는 함수를 지칭
    • 서브루틴은 한 번 호출되면 끝까지 실행됨
    • 따라서 루틴에 의해 서브루틴이 호출되면 루틴을 실행하던 쓰레드는 서브루틴을 실행하는 데 사용되어 서브루틴의 실행이 완료될 때까지 루틴은 다른 작업을 할 수 없음 (blocking)

 

2. 서브루틴과 코루틴의 차이

  • 서브루틴과 달리 코루틴은 함께(co) 실행되는 루틴으로 서로 간에 쓰레드 사용을 양보하며 함께 실행됨

 

 

부연 설명

  • 부모 코루틴은 while 문을 통해 `부모 코루틴에서 작업 실행 중`을 출력하고 쓰레드 사용 권한을 양보하는 yield 함수를 호출하는 동작을 무한 반복
  • 자식 코루틴도 while 문을 통해 `자식 코루틴에서 작업 실행 중`을 출력하고 yield 함수를 호출해 쓰레드 사용 권한을 양보하는 동작을 무한 반복
  • 각 코루틴이 쓰레드 사용 권한을 양보할 때마다 쓰레드가 필요한 다른 코루틴이 쓰레드 사용 권한을 가져가 실행
    • 서브루틴과 달리 코루틴은 서로 간에 협력적으로 동작하는 것이 핵심

 

코루틴의 쓰레드 양보

  • 쓰레드에 코루틴을 할당해 실행되도록 만드는 주체는 CoroutineDispatcher 객체이지만 쓰레드를 양보하는 주체는 코루틴
    • CoroutineDispatcher는 코루틴이 쓰레드를 양보하도록 강제하지 못함

 

  • 코루틴이 쓰레드를 양보하려면 코루틴에서 직접 쓰레드 양보를 위한 함수를 호출해야 하며 쓰레드를 양보를 일으키는 대표적인 일시 중단 함수들은 다음과 같음
    • delay
    • join & await
    • yield

 

1. delay 일시 중단 함수를 통해 알아보는 쓰레드 양보

  • 작업을 일정 시간 동안 일시 중단할 경우 delay 일시 중단 함수 사용
    • 코루틴이 delay 함수 호출 시 코루틴은 사용하던 쓰레드를 양보하고 설정된 시간 동안 코루틴을 일시 중단시킴

 

 

부연 설명

  • delay 함수는 메인 쓰레드 사용을 양보하기 때문에 10개의 코루틴이 거의 동시에 시작되는 것이 특징
  • 만약 delay 함수를 호출했을 때 쓰레드 양보가 일어나지 않았다면 작업을 모두 실행하는 데 10초가 걸렸을 것
    • 실제로 delay 대신 Thread.sleep(1000L)을 사용할 경우 10초가 걸리는 것을 확인 가능
    • Thread.sleep 함수는 쓰레드를 대기 시간 동안 점유하며 블로킹시키기 때문

 

 

2. join과 await의 동작 방식 자세히 알아보기

  • Job의 join 함수나 Deferred의 await 함수가 호출되면 해당 함수를 호출한 코루틴은 쓰레드를 양보하고
  • join 또는 await의 대상이 된 코루틴 내부의 코드가 실행 완료될 때까지 일시 중지됨

 

 

부연 설명

  • runBlocking 코루틴과 launch 코루틴은 단일 쓰레드인 메인 쓰레드에서 실행되기 때문에 하나의 코루틴이 쓰레드를 양보하지 않으면 다른 코루틴이 실행되지 못하기 때문에 각 코루틴은 다음 순서로 동작함
    • 처음 메인 쓰레드를 점유하는 것은 runBlocking 코루틴이며 해당 코루틴은 launch 함수를 호출해 launch 코루틴을 생성하지만 launch 코루틴 생성 후에도 runBlocking 코루틴이 계속해서 메인 쓰레드를 점유하기 때문에 launch 코루틴은 실행 대기 상태에 머뭄
    • 3번 로그 출력 후 job.join()을 실행하면 비로소 메인 쓰레드가 양보되며 launch 코루틴이 실행되어 1번 로그를 출력하고 이어서 delay 일시 중단 함수를 호출해 메인 쓰레드를 양보
    • 하지만 runBlocking 코루틴은 job.join()에 의해 launch 코루틴이 실행 완료될 때까지 재개되지 못하므로 실행되지 못함
    • launch 코루틴은 1초 동안 일시 중단 시간 후 지개되어 2번 로그가 출력되어 실행이 완료됨
    • launch 코루틴의 실행이 완료되면 runBlocking 코루틴은 재개돼 4번 로그를 출력

 

3. yield 함수 호출해 쓰레드 양보하기

  • 앞서 다룬 delay, join 같은 일시 중단 함수들은 쓰레드 양보를 직접 호출하지 않아도 작업을 위해 내부적으로 쓰레드 양보를 일으키는 반면 yield 함수는 개발자가 명시적으로 쓰레드 양보를 실행할 때 사용하는 함수

 

 

부연 설명

  • runBlocking 코루틴이 delay 일시 중단 함수를 호출해 메인 쓰레드를 양보하면 launch 코루틴이 메인 쓰레드를 점유하고 양보하지 않기 때문에 yield() 함수를 호출하지 않을 경우 runBlocking 코루틴의 나머지 코드인 job.cancel()은 실행되지 못함
  • launch 코루틴은 while 문 내부에서 yield()를 호출해 명시적으로 쓰레드를 양보
    • launch 코루틴이 양보한 메인 쓰레드는 runBlocking 코루틴의 나머지 코드를 실행하는 데 사용돼 job.cancel()이 호출됨

 

코루틴의 실행 쓰레드

 

1. 코루틴의 실행 쓰레드는 고정이 아니다

  • 앞서 설명했다시피 코루틴이 일시 중단 후 재개되면 CoroutineDispatcher 객체는 재개된 코루틴을 다시 쓰레드에 할당함
    • 이때 CoroutineDispatcher 객체는 코루틴을 자신이 사용할 수 있는 쓰레드 중 하나에 할당하는데 해당 쓰레드는 코루틴이 일시 중단 전에 실행되던 쓰레드와 다를 수 있음

 

 

부연 설명

  • newFixedThreadPoolContext 함수를 통해 MyThread-1, MyThread-2 쓰레드로 구성된 쓰레드 풀을 사용하는 CoroutineDispatcher 객체를 생성함
  • coroutine#2는 MyThread-1 쓰레드에서 실행될 때도 있고, MyThread-2 쓰레드에서 실행될 때도 있는 것을 확인 가능
  • coroutine#2의 실행 쓰레드가 바뀌는 이유는 coroutine#2가 재개될 때 CoroutineDispatcher 객체가 자신이 사용할 수 있는 쓰레드 중 하나에 coroutine#2를 보내기 때문
    • 코루틴의 실행 쓰레드가 변경되는 시점은 재개 시점

 

2. 쓰레드를 양보하지 않으면 실행 쓰레드가 바뀌지 않는다

  • 앞서 설명했다시피 코루틴의 실행 쓰레드가 바뀌는 시점은 코루틴이 재개되는 시점
    • 코루틴이 쓰레드 양보를 하지 않아 일시 중단될 일이 없다면 실행 쓰레드가 바뀌지 않음
    • 따라서 delay 대신 Thread.sleep() 함수를 사용하면 코루틴은 대기 시간 동안 쓰레드를 양보하지 않아 실행 쓰레드가 바뀌지 않음
    • 코루틴이 쓰레드를 양보하지 않으면 코루틴을 사용하는 이점이 모두 사라지게 되므로 지양해야 함

 

참고

코틀린 코루틴의 정석 (조세영 저)

반응형