관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석 - 일시 중단 함수 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석 - 일시 중단 함수

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

ㅁ 들어가며

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

9장 일시 중단 함수

- 일시중단함수의 개념 
- 일시중단함수 사용법
- 일시중단함수에서 코루틴을 실행하는 방법
- 일시중단함수의 호출지점

 

 

ㅁ 일시중단함수란?

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

// 일시 중단 함수 생성
suspend fun delayAndPrint(startTime:Long) {
  delay(1000L)
  println("[${Thread.currentThread().name}] [${getElapsedTime(startTime)}] 일시 중단 함수 실행")
}
/*
[main @coroutine#1] [지난 시간: 1012ms] 일시 중단 함수 실행
[main @coroutine#1] [지난 시간: 2035ms] 일시 중단 함수 실행
*/

ㅇ 일시중단함수는 suspend fun로 선언되는 함수 내에 일시중단지점을 포함할 수 있는 기능을 한다. 

ㅇ 일시중단함수는 코루틴은 아니다. runBlocking 코루틴 빌더함수를 호출하는 부분 외에 코루틴을 생성하는 부분이 없다. 

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  repeat(2){
    launch {
      delayAndPrint(startTime)
    }
  }
  println("[${Thread.currentThread().name}] [${getElapsedTime(startTime)}] root 실행")
}

suspend fun delayAndPrint(startTime:Long) {
  delay(1000L)
  println("[${Thread.currentThread().name}] [${getElapsedTime(startTime)}] 일시 중단 함수 실행")
}
/*
[main @coroutine#1] [지난 시간: 4ms] root 실행
[main @coroutine#2] [지난 시간: 1020ms] 일시 중단 함수 실행
[main @coroutine#3] [지난 시간: 1021ms] 일시 중단 함수 실행
*/

ㅇ 코루틴 빌더 함수인 launch를 호출하여 코투틴을 실행하면 main 스레드가 사용권을 양보하여 코루틴을 병렬로 처리할 수 있다.

 

ㅁ 일시중단함수의 사용

코루틴 내부에서 일시중단함수 호출하기

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  delayAndPrint(startTime, "1")
  launch {
    delayAndPrint(startTime, "2-1")
    delayAndPrint(startTime, "2-2")
  }
  launch {
    delayAndPrint(startTime, "3-1")
    delayAndPrint(startTime, "3-2")
  }
}

suspend fun delayAndPrint(startTime:Long, key: String) {
  delay(1000L)
  println("[${Thread.currentThread().name}] [${getElapsedTime(startTime)}] [${key}] 일시 중단 함수 실행")
}
/*
[main @coroutine#1] [지난 시간: 1008ms] [1] 일시 중단 함수 실행
[main @coroutine#2] [지난 시간: 2027ms] [2-1] 일시 중단 함수 실행
[main @coroutine#3] [지난 시간: 2028ms] [3-1] 일시 중단 함수 실행
[main @coroutine#2] [지난 시간: 3030ms] [2-2] 일시 중단 함수 실행
[main @coroutine#3] [지난 시간: 3030ms] [3-2] 일시 중단 함수 실행
*/

ㅇ 코드 수행 결과를 보면 각 코루틴이 일시 중단 함수를 잘 실행하였다. 

 

일시중단함수 안에서 일시 중단함수 호출하기 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  delayAndPrint(startTime, "1")
  launch {
    delayAndPrint(startTime, "re1")
  }
  launch {
    delayAndPrint(startTime, "re2")
  }
}

// 일시중단함수 안에 일시중단함수 호출하기를 재귀형태로 구현
suspend fun delayAndPrint(startTime:Long, key: String) {
  delay(1000L)
  println("[${Thread.currentThread().name}] [${getElapsedTime(startTime)}] [${key}] 일시 중단 함수 실행")
  if (key.startsWith("re")) delayAndPrint(startTime, "*"+key)
}
/*
[main @coroutine#1] [지난 시간: 1009ms] [1] 일시 중단 함수 실행
[main @coroutine#2] [지난 시간: 2039ms] [re1] 일시 중단 함수 실행
[main @coroutine#3] [지난 시간: 2042ms] [re2] 일시 중단 함수 실행
[main @coroutine#2] [지난 시간: 3045ms] [*re1] 일시 중단 함수 실행
[main @coroutine#3] [지난 시간: 3046ms] [*re2] 일시 중단 함수 실행
*/

ㅇ 일시중단함수 안에서 일시중단함수를 수행하여도 main 스레드가 병렬로 처리하고 있다.

 

일시중단함수에서 코루틴실행하기

ㅇ 일시중단함수에서 코루틴 빌더 호출 시 같은 coroutine body에 있어야 한다.

ㅇ 더 정확히 말하자면, 제어권이 공유 가능한 서로 같은 CoroutineScope로 묶여야 한다.

ㅇ 그 이유는 일시중단함수 내부에서는 일시 중단 함수를 호출한 코루틴의 CoroutineScope에 접근할 수 없기 때문이다.

ㅇ 그래서 일시중단함수에서 코루틴 빌더 함수인 launch나 async 함수를 호출할 수 있도록 같은 CoroutineScope객체에 접근하도록 해야한다. 

 

coroutineScope 사용해 일시중단함수에서 코루틴 실행하기

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

  // 2. 작업
  val results = 상위작업("작업")
  println("[결과] ${results.toList()}") // 결과
  println(getElapsedTime(startTime)) // 지난 시ㅌ간 표시
}

suspend fun 상위작업(keyword: String): Array<String> = coroutineScope { // this: CoroutineScope
  val 하위작업1Deferred = async {
    하위작업1(keyword)
  }
  val 하위작업2Deferred = async {
    하위작업2(keyword)
  }

  return@coroutineScope arrayOf(*하위작업1Deferred.await(), *하위작업2Deferred.await())
}

suspend fun 하위작업1(keyword: String): Array<String> {
  delay(1000L)
  return arrayOf("[하위작업1]${keyword}1", "[하위작업1]${keyword}2")
}

suspend fun 하위작업2(keyword: String): Array<String> {
  delay(1000L)
  return arrayOf("[하위작업2]${keyword}1", "[하위작업2]${keyword}2")
}

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

/*
[결과] [[하위작업1]작업1, [하위작업1]작업2, [하위작업2]작업1, [하위작업2]작업2]
지난 시간: 1044ms
*/

ㅇ runBlocking 코루틴에서 상위작업의 일시중단 함수를 호출하면서 내부에서 coroutineScop  아래에 새로운 Job객체를 가진 CoroutineScope가 생성되고, 그 하위작업이 각각 Deferred 하위작업을 구현한다.

 

// ~~~
suspend fun 상위작업(keyword: String): Array<String> = coroutineScope { // this: CoroutineScope
  val 하위작업1Deferred = async {
    throw Exception("하위작업1 예외 발생")
    하위작업1(keyword)
  }
  val 하위작업2Deferred = async {
    하위작업2(keyword)
  }

  return@coroutineScope arrayOf(*하위작업1Deferred.await(), *하위작업2Deferred.await())
}
// ~~~
/*
Exception in thread "main" java.lang.Exception: 하위작업1 예외 발생
...
Caused by: java.lang.Exception: 하위작업1 예외 발생
...
*/

ㅇ 여기서 문제점은 하위작업에서 예외가 발생하면 전체가 취소대 버린다.

ㅇ 이를 해결하기 위해서는 supervisorScope가 필요하다.

 

supervisorScope를 사용해 일시중단함수에서 코루틴 실행

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

  // 2. 작업
  val results = 상위작업("작업")
  println("[결과] ${results.toList()}") // 3. 결과값 출력
  println(getElapsedTime(startTime)) // 4. 지난 시간 표시
}

suspend fun 상위작업(keyword: String): Array<String> = supervisorScope { // this: CoroutineScope
  val 하위작업1Deferred = async {
    throw Exception("하위작업1 예외 발생")
    하위작업1(keyword)
  }
  val 하위작업2Deferred = async {
    하위작업2(keyword)
  }

  // try문 추가하여 예외 대응으로 빈값 리턴
  val 하위작업1Results = try {
    하위작업1Deferred.await()
  } catch (e: Exception) {
    arrayOf() 
  }
  
  // try문 추가하여 예외 대응으로 빈값 리턴
  val 하위작업2Results = try {
    하위작업2Deferred.await()
  } catch (e: Exception) {
    arrayOf() // 에러 발생 시 빈 결과 반환
  }

  return@supervisorScope arrayOf(*하위작업1Results, *하위작업2Results)
}

suspend fun 하위작업1(keyword: String): Array<String> {
  delay(1000L)
  return arrayOf("[하위작업1]${keyword}1", "[하위작업1]${keyword}2")
}

suspend fun 하위작업2(keyword: String): Array<String> {
  delay(1000L)
  return arrayOf("[하위작업2]${keyword}1", "[하위작업2]${keyword}2")
}

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

/*
[결과] [[하위작업2]작업1, [하위작업2]작업2]
지난 시간: 1054ms
*/

 

ㅁ 함께 보면 좋은 사이트

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

반응형
Comments