관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석 - 코루틴의 이해 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석 - 코루틴의 이해

기록하는 백앤드개발자 2024. 6. 7. 10:10
반응형

ㅁ 들어가며

 코틀린 코루틴의 정석 책을 보고 정리한 글입니다.

10장 코루틴의 이해

- 루틴, 서브루틴, 코루틴의 이해
- 코루틴의 스레드 양보
- 고정적이지 않은 코루틴의 실행 스레드

 

ㅁ 서브루틴과 코루틴

ㅇ 서브루틴은 함수 안에서 호출되는 함수를 말한다. 

ㅇ 일반적으로 루틴이라는 단어는 '특정한 일을 처리하는 과정'을 의미한다. 

ㅇ 프로그래밍에서는 루틴을 '특정한 일을 처리하기 위한 일련의 명령'을 의미하며, 이런 명령을 함수 또는 메서드라고 한다.

 

fun main() = runBlocking {
  val startTime = System.currentTimeMillis()
  //메인 루틴
  routine(startTime)
}

fun routine(startTime:Long) {
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] main Routine")
  // 서브 루틴  
  subRoutineA(startTime)
  subRoutineB(startTime)
}

fun subRoutineA(startTime:Long) {
  Thread.sleep(1000L);
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] subRoutineA")
}

fun subRoutineB(startTime:Long) {
  Thread.sleep(1000L)
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] subRoutineB")
}

fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

/*
[main @coroutine#1][지난 시간: 0ms] main Routine
[main @coroutine#1][지난 시간: 1012ms] subRoutineA
[main @coroutine#1][지난 시간: 2016ms] subRoutineB
 */

ㅇ 서브루틴은 루틴의 하위에서 실행되는 루틴이다. 즉, 함수 내부에서 호출되는 함수를 서브루틴이라고 한다.

ㅇ 서브루틴은 한 번 실행되면 끝까지 하나의 main 스레드에서 실행되면서, 순차적으로 실행되어 총 2초가 실행된다.

ㅇ 이 코드를 코루틴 형태로 바꾸어 보자

 

ㅁ 코루틴과 서브루틴의 차이

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  launch {
    routine(startTime)
  }
}

suspend fun routine(startTime:Long) = coroutineScope {
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] main Routine")
  launch {
    subRoutineA(startTime)
  }
  launch {
    subRoutineB(startTime)
  }
}

suspend fun subRoutineA(startTime:Long) {
  delay(1000L);
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] subRoutineA")

}

suspend fun subRoutineB(startTime:Long) {
  delay(1000L)
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] subRoutineB")
}
/*
[main @coroutine#2][지난 시간: 3ms] main Routine
[main @coroutine#3][지난 시간: 1019ms] subRoutineA
[main @coroutine#4][지난 시간: 1020ms] subRoutineB
 */
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"

ㅇ 코루틴은 서로 간에 스레드 사용 권한을 양보하며 함께 실행된다.

ㅇ delay함수는 스레드를 양보하고 일정 시간 동안 코루틴을 일시 중단시킨다.

ㅇ 서브루틴은 스레드 양보를 통해  병렬처리가 가능하여 총 1초에 작업이 완료되었다.

 

ㅇ Thread.sleep은 스레드를 점유상태로 스레드는 다른 일을 할 수 없지만, delay를 통해 스레드 양보를 통해 스레드가 다른 작업을 수행할 수 있다. 

 

ㅁ 코루틴의 스레드 양보

ㅇ 코루틴은 제어권 양보를 통해 스레드 자원을 공유한다.

ㅇ 코루틴이 스레드를 양보하기 위한 방법은 다음 3가지이다.

  ㄴ delay 함수: 위의 코드가 delay 함수 이용

  ㄴ join과 await 동작 방식

  ㄴ yield 함수

 

join과 await 동작 방식

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val job = launch {
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] launch 코루틴 작업이 시작됐습니다")
    delay(100L) // 대기
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] launch 코루틴 작업이 완료됐습니다")
  }
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] runBlocking 코루틴이 곧 일시 중단 되고 메인 스레드가 양보됩니다")
  job.join() // job 작업 대기
  println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] runBlocking이 메인 스레드에 분배돼 작업이 다시 재개됩니다")
}
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"
/*
[main @coroutine#1][지난 시간: 2ms] runBlocking 코루틴이 곧 일시 중단 되고 메인 스레드가 양보됩니다
[main @coroutine#2][지난 시간: 11ms] launch 코루틴 작업이 시작됐습니다
[main @coroutine#2][지난 시간: 117ms] launch 코루틴 작업이 완료됐습니다
[main @coroutine#1][지난 시간: 118ms] runBlocking이 메인 스레드에 분배돼 작업이 다시 재개됩니다
*/

ㅇ join과 await 함수를 호출한 코루틴은 join이나 await의 대상이 된 코루틴의 작업이 완료될 때까지 스레드를 양보하고 일시 중단한다.

ㅇ job.join()의 경우 순서보장을 위해 job이 완료될 때까지 main 스레드가 다른 작업을 수행하지 않고 대기한다. 

 

yield 함수

fun main() = runBlocking<Unit> {
  launch {
    repeat(5) { i ->
      println("[${Thread.currentThread().name}] ${i} 서브 코루틴 실행")
      yield() // 스레드 양보
    }
  }

  repeat(5) { i ->
    println("[${Thread.currentThread().name}] ${i} 코루틴 실행")
    yield() // 스레드 양보
  }
}
/*
[main @coroutine#1] 0 코루틴 실행
[main @coroutine#2] 0 서브 코루틴 실행
[main @coroutine#1] 1 코루틴 실행
[main @coroutine#2] 1 서브 코루틴 실행
[main @coroutine#1] 2 코루틴 실행
[main @coroutine#2] 2 서브 코루틴 실행
[main @coroutine#1] 3 코루틴 실행
[main @coroutine#2] 3 서브 코루틴 실행
[main @coroutine#1] 4 코루틴 실행
[main @coroutine#2] 4 서브 코루틴 실행
*/

 

ㅇ yield 함수는 스레드 사용 권한을 명시적으로 양보하고자 할 때 사용된다.

 

fun main() = runBlocking<Unit> {
  val job = launch {
    while (this.isActive) {
      println("무한 작업 중")
    }
  }
  delay(100L) // 100밀리초 대기(스레드 양보)
  job.cancel() // 코루틴 취소가 job이 작업하는 동안 실행이 되지 않는다.
}
/*
무한 작업 중
무한 작업 중
무한 작업 중
...
*/

ㅇ delay함수가 메인 스레드를 양보하면 launch 코루틴이 실행된다.

ㅇ 하지만 명시적인 yield를 호출하지 않아 무한 작업에 빠지게 된다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val dispatcher = newFixedThreadPoolContext(2, "MyThread")
  launch(dispatcher) {
    repeat(5) { i ->
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] ${i} 코루틴 실행 일시중단")
      delay(100L) // delay 함수를 통해 launch 코루틴을 100밀리초간 일시 중단한다.
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] ${i} 코루틴 실행 재개")
    }
  }
}
/*
[MyThread-1 @coroutine#2][지난 시간: 5ms] 0 코루틴 실행 일시중단
[MyThread-2 @coroutine#2][지난 시간: 122ms] 0 코루틴 실행 재개
[MyThread-2 @coroutine#2][지난 시간: 122ms] 1 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 227ms] 1 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 227ms] 2 코루틴 실행 일시중단
[MyThread-2 @coroutine#2][지난 시간: 333ms] 2 코루틴 실행 재개
[MyThread-2 @coroutine#2][지난 시간: 333ms] 3 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 438ms] 3 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 438ms] 4 코루틴 실행 일시중단
[MyThread-2 @coroutine#2][지난 시간: 543ms] 4 코루틴 실행 재개
*/

ㅇ 코루틴은 협력적 동작은 스레드 제어권을 양도하면서 다른 코루틴이 스레드를 사용할 수 있도록 한다.

 

ㅇ 코루틴이 스레드를 양보하면 코루틴은 일시 중단되며, 재개될 때 Coroutine Dispatcher 객체를 통해 다시 스레드에 보내진다.

ㅇ CoroutineDispatcher 객체는 스레드풀에서 유휴한 스레드를 할당하기 때문에 고정된 스레드가 아니다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val dispatcher = newFixedThreadPoolContext(2, "MyThread")
  launch(dispatcher) {
    repeat(5) { i ->
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] ${i} 코루틴 실행 일시중단")
      Thread.sleep(100L) // delay를 Thread.sleep으로 수정
      println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] ${i} 코루틴 실행 재개")
    }
  }
}
/*
[MyThread-1 @coroutine#2][지난 시간: 7ms] 0 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 116ms] 0 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 116ms] 1 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 221ms] 1 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 221ms] 2 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 326ms] 2 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 327ms] 3 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 428ms] 3 코루틴 실행 재개
[MyThread-1 @coroutine#2][지난 시간: 428ms] 4 코루틴 실행 일시중단
[MyThread-1 @coroutine#2][지난 시간: 532ms] 4 코루틴 실행 재개
*/

ㅇ delay를 Thread.sleep으로 수정하였다.

ㅇ 코루틴이 스레드를 양보하지 않으면 실행 스레드가 바뀌지 않는다.

ㅇ 코루틴 내부에서 Thread.sleep 함수를 사용하면 코루틴이 대기하는 시간 동안 스레드를 양보하지 않고 블로킹한다.

 

ㅁ 함께 보면 좋은 사이트

 조세영 - 코틀린 코루틴의 정석

반응형
Comments