이번 포스트에서는 코드로 kotlin coroutine을 사용해보면서 특징을 파악합니다
크게 1,2로 나누어서 포스팅할 예정입니다
포스팅 내용은 코루틴을 익히면서 고도화 될 예정입니다
다루는 내용
- runBlocking
- 코루틴 취소
- 코루틴 예외처리
runBlocking
kotlin에서는 다음과 같이 runBlocking으로 코루틴을 생성할 수 있습니다
runBlocking 내에서는 코루틴을 사용할 수 있게 됩니다
fun main () : Unit = runBlocking {
printWithThread("START")
launch { newRoutine() }
printWithThread("END")
}
suspend fun newRoutine() {
val num1 = 1
val num2 = 2
printWithThread("Sum: ${num1+ num2}}")
}
runBlocking 특징
runBlocking은 블로킹 코드 실행이 끝날때 까지 기다림
runBlocking은 기본적으로 호출된 스레드(일반적으로 메인 스레드)를 차단하고, 그 스레드에서 코루틴을 실행합니다. 특별한 CoroutineScope나 Dispatcher를 지정하지 않으면, runBlocking 내부의 코루틴은 다음과 같은 특성을 가집니다
- 단일 스레드 실행: runBlocking 내부의 코루틴은 기본적으로 호출된 스레드에서 실행됩니다.
- 순차적 실행: 코루틴들은 기본적으로 순차적으로 실행됩니다. 즉, 한 코루틴이 일시 중단되면 다음 코루틴이 실행될 수 있습니다.
- Dispatchers.Default 사용 안 함: 특별히 지정하지 않으면 Dispatchers.Default와 같은 다른 디스패처를 사용하지 않습니다.
//출력결과:
//[2024-08-24T05:43:30.774408][main @coroutine#1] START
//[2024-08-24T05:43:32.786927][main @coroutine#2] Launch end
//[2024-08-24T05:43:32.789474][main] END
fun main() {
runBlocking {
printWithThread("START")
launch {
delay(2_000L)
printWithThread("Launch end")
}
}
printWithThread("END")
}
lazy execution
//출력결과:
//[2024-08-24T05:43:12.452365][main @coroutine#1] START
//[2024-08-24T05:43:13.461081][main @coroutine#2] lazy execution
fun main(): Unit = runBlocking {
printWithThread("START")
val job = launch(start = CoroutineStart.LAZY) {
printWithThread("lazy execution")
}
delay(1_000L)
job.start()
}
코루틴 비동기 실행
//출력결과:
//[2024-08-24T05:42:11.651632][main @coroutine#1] START
//[2024-08-24T05:42:12.662338][main @coroutine#2] job1
//[2024-08-24T05:42:12.662798][main @coroutine#3] job2
fun main(): Unit = runBlocking {
printWithThread("START")
val job1 = launch {
delay(1_000L)
printWithThread("job1")
}
val job2 = launch {
delay(1000L)
printWithThread("job2")
}
}
코루틴 순차 블록킹 실행
//출력결과:
//[2024-08-24T05:45:15.705073][main @coroutine#1] START
//[2024-08-24T05:45:16.715395][main @coroutine#2] job1
//[2024-08-24T05:45:17.721882][main @coroutine#3] job2
//[2024-08-24T05:45:17.722563][main @coroutine#1] END
fun main(): Unit = runBlocking {
printWithThread("START")
val job1 = launch {
delay(1_000L)
printWithThread("job1")
}
job1.join()
val job2 = launch {
delay(1000L)
printWithThread("job2")
}
job2.join()
printWithThread("END")
}
코루틴 결과를 받을때는 async 사용
fun main(): Unit = runBlocking {
val job = async {
3 + 5
}
val result = job.await()
printWithThread("Result: $result")
}
코루틴 내에서 다른 코루틴 호출
//출력결과:
//[2024-08-24T05:54:30.219526][main @coroutine#1] START
//[2024-08-24T05:54:30.229829][main @coroutine#2] asyncReturn1
//[2024-08-24T05:54:31.235452][main @coroutine#3] asyncReturn2
//[2024-08-24T05:54:32.238094][main @coroutine#1] Result: 2
fun main(): Unit = runBlocking {
printWithThread("START")
val job1 = async { asyncReturn1() }
val job2 = async { asyncReturn2(job1.await()) }
val result2 = job2.await()
printWithThread("Result: ${result2}")
}
suspend fun asyncReturn1(): Int {
printWithThread("asyncReturn1")
delay(1_000L)
return 1
}
suspend fun asyncReturn2(num: Int): Int {
printWithThread("asyncReturn2")
delay(1_000L)
return num+1
}
코루틴 취소
기본적으로 코루틴을 취소하려면 delay, yield같은 kotlinx.coroutins 패키지의 suspend 함수를 사용해야합니다
//출력결과:
//[2024-08-24T05:42:54.086875][main @coroutine#1] START
//[2024-08-24T05:42:54.093769][main @coroutine#2] Count: 1
//[2024-08-24T05:42:54.599408][main @coroutine#2] Count: 2
fun main(): Unit = runBlocking {
printWithThread("START")
val job = launch {
(1..5).forEach{
printWithThread("Count: $it")
delay(500L)
}
}
delay(1_000L)
job.cancel()
}
아래 코드 결과 맞춰보기
//출력결과
//[2024-08-25T23:25:15.822368][main @coroutine#2] Count: 1
//[2024-08-25T23:25:16.816044][main @coroutine#2] Count: 2
//[2024-08-25T23:25:17.816031][main @coroutine#2] Count: 3
// ... 5까지 출력되고 해당 코루틴은 종료됌
fun main(): Unit = runBlocking {
val job1 = launch {
var i = 1
var nextPrintTime = System.currentTimeMillis()
while (i <= 5) {
if (nextPrintTime <= System.currentTimeMillis()) {
printWithThread("Count: ${i++}")
nextPrintTime += 1000L
}
if (!isActive) {
throw CancellationException()
}
}
}
delay(100L)
job1.cancel()
}
위코드는
launch에 CoroutinScope를 별도로 전달하지 않으면 같은 스레드에서 동작하니 해당 로직은 무한루프이기 때문에 delay코드가 동작안합니다
CoroutineContext를 분리해서 CancellationException을 던져서 취소시키기
// 출력결과
// [2024-08-25T23:39:43.078393][DefaultDispatcher-worker-1 @coroutine#2] Count: 1
fun main(): Unit = runBlocking {
val job1 = launch(Dispatchers.Default) {
var i = 1
var nextPrintTime = System.currentTimeMillis()
while (i <= 5) {
if (nextPrintTime <= System.currentTimeMillis()) {
printWithThread("Count: ${i++}")
nextPrintTime += 1000L
}
if (!isActive) {
throw CancellationException()
}
}
}
delay(100L)
job1.cancel()
}
launch와 runBlocking 이 서로 다른 스레드에서 코루틴이 동작하기 때문에 launch와 delay가 서로 다른 스레드에서 실행되기 때문에
cancel이 바로 동작하여 isActive가 false라서 해당 분기에서 CancellationException이 발생합니다
delay()메서드도 내부적으로 CancellationException을 던지고 있었음
// 출력결과:
//[2024-08-25T23:44:43.006158][main @coroutine#1] 취소 시도
//[2024-08-25T23:44:43.900574][main @coroutine#2] delay로 취소되지 않음!
fun main(): Unit = runBlocking {
val job1 = launch {
try {
delay(1_000L)
} catch (e: CancellationException) {
// do nothing!!
}
printWithThread("delay로 취소되지 않음!")
}
delay(100L)
printWithThread("취소 시도")
}
코루틴 예외처리
//출력결과:
// Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.RuntimeException
// at MainKt$main$1$job$1.invokeSuspend(Main.kt:276)...
fun main(): Unit = runBlocking {
val job = CoroutineScope(Dispatchers.Default).launch {
throw RuntimeException()
}
delay(1000L)
}
async 사용시 예외발생하지 않음
// 출력결과:
// Exception in thread "main" java.lang.RuntimeException
// at MainKt$main$1$job$1.invokeSuspend(Main.kt:287)
// at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:46)
// at MainKt$main$1.invokeSuspend(Main.kt:291)
// 예외가 발생해도, 예외를 출력하지 않고
// 예외를 확인하려면 await()이 필요합니다
fun main(): Unit = runBlocking {
val job = CoroutineScope(Dispatchers.Default).async {
throw RuntimeException()
}
delay(1000L)
job.await()
}
async 사용시 예외발생하지 않음은 아닙니다
단지 예외를 출력하지 않았을 뿐이고 async한 코루틴 job(DeferredCoroutine)에 대해서 await()를 해주면 예외가 출력됩니다
자식 코루틴의 예외는 부모 코루틴에 전달된다
//출력결과:
//Exception in thread "main" java.lang.RuntimeException
// at MainKt$main$1$job$1.invokeSuspend(Main.kt:302)
// at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
// at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
// at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
// at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
// at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
// at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
// at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
// at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
// at MainKt.main(Main.kt:300)
// at MainKt.main(Main.kt)
// 자식 코루틴의 예외는 부모코루틴으로 전파된다
fun main(): Unit = runBlocking {
val job = async {
throw RuntimeException()
}
}
만약 자식 코루틴의 예외를 부모 코루틴에 전파하고 싶지 않은경우에?
// SupervisorJob을 사용하면, 자식 코루틴의 예외를 부모 코루틴으로 전파하지 않음
fun main(): Unit = runBlocking {
val job = async(SupervisorJob()) {
throw RuntimeException()
}
delay(1000L)
}
CoroutineExceptionHandler 사용
// [2024-08-25T15:53:17.273475][DefaultDispatcher-worker-1 @coroutine#2] Caught java.lang.RuntimeException
//
//Process finished with exit code 0
//fun main(): Unit = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
printWithThread("Caught $throwable")
}
val job = CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
throw RuntimeException()
}
delay(1000L)
}
CoroutineExceptionHandler 주의할점
launch에만 적용 가능하며, 부모 코루틴이 있으면 동작하지 않음
// exceptionHandler
// [2024-08-25T15:54:05.694514][DefaultDispatcher-worker-1 @coroutine#2] Caught java.lang.RuntimeException
//Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.RuntimeException
// at MainKt$main$1$job$1.invokeSuspend(Main.kt:351)
// at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
// at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
// at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
// at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
// at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
// at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
// Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [MainKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@540a1eaa, CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@7b649469, Dispatchers.Default]
// launch에만 적용 가능하며, 부모 코루틴이 있으면 동작하지 않음
fun main(): Unit = runBlocking {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
printWithThread("Caught $throwable")
throw throwable
}
val job = CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
throw RuntimeException()
}
delay(1000L)
}
CancellationException인 경우 코루틴 취소로 간주하고, 부모 코루틴에 전파 하지 않으
// 발생한 예외가 CancelationException인 경우 부모 코루틴은 예외를 잡지 않음
// 그 외 다른 예외가 발생하는 경우 실패로 간주하고 부모 코루틴에 전파
fun main() = runBlocking {
try {
coroutineScope {
// CancellationException을 발생시키는 자식 코루틴
launch {
println("자식 1: CancellationException 발생")
throw CancellationException("자식 1 취소됨")
}
// 일반 예외를 발생시키는 자식 코루틴
launch {
println("자식 2: RuntimeException 발생")
throw RuntimeException("자식 2 에러")
}
// 정상적으로 완료되는 자식 코루틴
launch {
println("자식 3: 정상 실행")
delay(100)
println("자식 3: 완료")
}
println("부모: 자식 코루틴 실행 중")
delay(200) // 자식 코루틴들이 실행될 시간을 줍니다
}
} catch (e: Exception) {
println("부모가 잡은 예외: ${e::class.simpleName} - ${e.message}")
}
println("메인: 완료")
}
Structured Concurrency
자식코루틴에서 예외가 발생했을때 부모 코루틴에 전달되고, 다른 자식 코루틴도 취소된다
//출력결과:
//부모: 시작
//자식 1: 시작
//자식 2: 시작
//자식 4: 시작
//자식 3: 시작
//자식 2: 작업 중...
//자식 4: 취소됨
//부모 Job: 예외로 인해 종료됨 - 자식 3에서 오류 발생!
//Exception in thread "main" java.lang.RuntimeException: 자식 3에서 오류 발생!
// at MainKt$main$2$job$1$child3$1.invokeSuspend(Main.kt:493)
suspend fun main() = coroutineScope {
val job = launch {
println("부모: 시작")
// 첫 번째 자식 코루틴
val child1 = launch {
println("자식 1: 시작")
delay(500)
println("자식 1: 작업 중...")
delay(500)
println("자식 1: 완료")
}
// 두 번째 자식 코루틴
val child2 = launch {
println("자식 2: 시작")
delay(300)
println("자식 2: 작업 중...")
delay(300)
println("자식 2: 완료")
}
// 세 번째 자식 코루틴 (예외 발생)
val child3 = launch {
println("자식 3: 시작")
delay(400)
throw RuntimeException("자식 3에서 오류 발생!")
}
// 네 번째 자식 코루틴 (취소 시연)
val child4 = launch {
try {
println("자식 4: 시작")
delay(2000) // 오래 실행되는 작업
println("자식 4: 이 메시지는 출력되지 않을 것입니다")
} finally {
println("자식 4: 취소됨")
}
}
// 자식 코루틴들이 완료될 때까지 기다림
joinAll(child1, child2, child3, child4)
println("부모: 모든 자식 작업 완료")
}
// 부모 Job이 완료될 때의 처리
job.invokeOnCompletion { throwable ->
when (throwable) {
null -> println("부모 Job: 정상적으로 완료됨")
is CancellationException -> println("부모 Job: 취소됨")
else -> println("부모 Job: 예외로 인해 종료됨 - ${throwable.message}")
}
}
delay(1000) // 일부 자식 코루틴이 실행을 마칠 시간을 줌
// 부모 Job 취소 (모든 자식도 취소됨)
job.cancel()
// 부모 Job이 완전히 종료될 때까지 기다림
job.join()
println("메인: 모든 작업 완료")
}
단 예외로 CancellationException은 예외가 발생해도 부모 코루틴에 전파하지 않음
//출력결과:
//[2024-08-26T07:53:29.012582][DefaultDispatcher-worker-1 @coroutine#1] 부모: 시작
//[2024-08-26T07:53:29.01427][DefaultDispatcher-worker-2 @coroutine#2] 자식 1: 시작
//[2024-08-26T07:53:29.01443][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: 시작
//[2024-08-26T07:53:29.01471][DefaultDispatcher-worker-4 @coroutine#4] 자식 3: 시작
//[2024-08-26T07:53:29.522315][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: CancellationException 발생 - 자식 2 취소
//[2024-08-26T07:53:29.522691][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: 정리 작업
//[2024-08-26T07:53:29.818435][DefaultDispatcher-worker-3 @coroutine#4] 자식 3: 예외 발생 - 자식 3에서 오류 발생!
//[2024-08-26T07:53:29.818591][DefaultDispatcher-worker-3 @coroutine#4] 자식 3: 정리 작업
//[2024-08-26T07:53:29.845433][DefaultDispatcher-worker-2 @coroutine#2] 자식 1: 정리 작업
//[2024-08-26T07:53:29.845529][DefaultDispatcher-worker-4 @coroutine#1] 부모: 자식에서 예외 잡음 - StandaloneCoroutine is cancelling
//[2024-08-26T07:53:29.845589][DefaultDispatcher-worker-4 @coroutine#1] 부모: 모든 자식 작업 완료
//[2024-08-26T07:53:29.84577][DefaultDispatcher-worker-2 @coroutine#2] 부모 Job: 예외로 인해 종료됨 - 자식 3에서 오류 발생!
//Exception in thread "main" java.lang.RuntimeException: 자식 3에서 오류 발생!
// at MainKt$main$2$job$1$child3$1.invokeSuspend(Main.kt:576)
suspend fun main() = coroutineScope {
val job = launch {
printWithThread("부모: 시작")
// 첫 번째 자식 코루틴 (정상 완료)
val child1 = launch {
try {
printWithThread("자식 1: 시작")
delay(1000)
printWithThread("자식 1: 완료")
} finally {
printWithThread("자식 1: 정리 작업")
}
}
// 두 번째 자식 코루틴 (CancellationException 발생)
val child2 = launch {
try {
printWithThread("자식 2: 시작")
delay(500)
throw CancellationException("자식 2 취소")
} catch (e: CancellationException) {
printWithThread("자식 2: CancellationException 발생 - ${e.message}")
} finally {
printWithThread("자식 2: 정리 작업")
}
}
// 세 번째 자식 코루틴 (일반 예외 발생)
val child3 = launch {
try {
printWithThread("자식 3: 시작")
delay(800)
throw RuntimeException("자식 3에서 오류 발생!")
} catch (e: Exception) {
printWithThread("자식 3: 예외 발생 - ${e.message}")
throw e // 예외를 다시 던져서 부모로 전파
} finally {
printWithThread("자식 3: 정리 작업")
}
}
try {
joinAll(child1, child2, child3)
} catch (e: Exception) {
printWithThread("부모: 자식에서 예외 잡음 - ${e.message}")
}
printWithThread("부모: 모든 자식 작업 완료")
}
job.invokeOnCompletion { throwable ->
when (throwable) {
null -> printWithThread("부모 Job: 정상적으로 완료됨")
is CancellationException -> printWithThread("부모 Job: 취소됨")
else -> printWithThread("부모 Job: 예외로 인해 종료됨 - ${throwable.message}")
}
}
job.join()
printWithThread("메인: 모든 작업 완료")
}
'kotlin > 기초' 카테고리의 다른 글
kotlin 문법 기초 - 4 (0) | 2024.07.23 |
---|---|
kotlin 문법 기초 - 3 (0) | 2024.07.17 |
kotlin 문법 기초 - 2 (1) | 2024.07.14 |
kotlin 문법 기초-1 (0) | 2024.07.10 |