티스토리 뷰
728x90
코루틴 흐름 제어
- Job
- 동시성 작업의 생명 주기를 표현하는 객체
- 작업 상태를 추적하고, 작업을 취소할 수 있다.
- 잡은 생성되자마자 시작되므로 Active 상태가 된다.
- isActive, isCompleted, isCancelled 프로퍼티를 통해 상태를 추적할 수 있다.
- Cancelled, Completed 상태일 때 isComplete의 프로퍼티 값이 모두 true다. isCancelled을 통해 두 상태를 구분한다.
- launch(), async()에서 잡의 초기 상태를 지정할 수 있다.
- CoroutineStart.DEFAULT: 잡을 즉시 시작한다. (default)
- CoroutineStart.LAZY: 잡을 자동으로 시작하지 않는다. New 상태에서 시작을 기다린다.
- start()
fun main() {
runBlocking {
val job = launch(start = CoroutineStart.LAZY) { // New
println("Job started")
}
delay(100)
job.start() // Active
}
}
-
- New 상태의 잡에 대해 start()를 호출하면 잡이 시작되면서 Active 상태가 된다.
- children
fun main() {
runBlocking {
val job = coroutineContext[Job.Key]!!
launch { println("task 1") }
launch { println("task 2") }
println("${job.children.count()} child running") // 2
}
}
-
- 잡이 다른 잡을 시작하면, 기존 잡이 새 잡의 부모가 된다.
- children 프로퍼티를 통해 완료되지 않은 자식 잡들을 얻을 수 있다.
- 코루틴이 본문을 끝내면 Completing 상태가 되어 자식들의 완료를 기다린다.
- 모든 자식이 완료되면 상태가 Completing에서 Completed로 바뀐다.
- join()
fun main() {
runBlocking {
val job = coroutineContext[Job.Key]!!
val t1 = launch { println("task 1") }
val t2 = launch { println("task 2") }
t1.join()
t2.join()
println("${job.children.count()} child running") // 0
}
}
-
- 조인 대상 잡이 완료될 때까지 현재 코루틴을 일시 중단한다.
- 조인 대상 잡이 New 상태라면 Active 상태가 되고 잡이 시작된다.
- cancel()
- cancel()이 호출되면, 코루틴의 취소를 확인하고 CancellationException을 발생시킨다.
suspend fun main() {
val job = GlobalScope.launch(Dispatchers.Default) {
var i = 1
while (true) { println(i++) }
}
delay(100)
job.cancel()
}
-
- 위 코루틴은 취소되지 않고 계속 실행된다.
suspend fun main() {
val job = GlobalScope.launch {
var i = 1
while (isActive) { println(i++) } // 취소됐는지 검사
}
delay(100)
job.cancel()
}
-
- isActive 프로퍼티를 통해 코루틴이 취소됐는지 검사한다.
fun main() = runBlocking {
val parent = launch {
GlobalScope.launch {
println("child 1 started")
delay(1000)
println("child 1 finished")
}
launch {
println("child 2 started")
delay(1000)
println("child 2 finished")
}
launch {
println("child 3 started")
delay(1000)
println("child 2 finished")
}
}
delay(500)
parent.cancel()
delay(1000)
println("parent finished")
}
실행 결과
child 1 started
child 2 started
child 3 started
child 1 finished
parent finished
|
-
- 부모 코루틴이 취소되면 자동으로 모든 자식의 실행을 취소한다.
- GlobalScope에서 실행되는 코루틴은 부모가 취소되어도 영향을 받지 않는다.
- 부모 코루틴은 GlobalScope 자식 코루틴이 끝날 때까지 기다려주지 않는다.
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("try $i")
delay(500L)
}
} finally {
withContext(NonCancellable) {
delay(1000)
println("finally")
}
}
}
delay(1300L)
job.cancelAndJoin()
}
실행 결과
try 0
try 1
try 2
finally
|
- 이미 취소된 코루틴의 finally 블록 안에서 suspend 함수를 호출하면, CancellationException이 발생한다.
- withContext로 NonCancellable context를 전달하여 처리할 수 있다.
- NonCancellable은 isActive가 항상 true기 때문에 취소되지 않는다.
2. 타임아웃
- withTimeout()
fun main() {
runBlocking {
val asyncData = async { File("data.txt").readText() }
try {
val text = withTimeout(50) { asyncData.await() }
println("Data loaded: $text")
} catch (e: Exception) {
println("Timeout exceeded")
}
}
}
-
- withTimeout() 함수를 통해 코루틴 작업에 대한 타임아웃을 설정한다.
- 50ms 안에 결과를 받아올 수 있으면 결과를 출력하고, 받아올 수 없다면 TimeoutCancellationException을 발생시킨다.
- withTimeoutOrNull()
fun main() {
runBlocking {
val asyncData = async { File("data.txt").readText() }
val text = withTimeoutOrNull(50) { asyncData.await() } ?: "Timeout"
println("Data loaded: $text")
}
}
-
- 타임아웃이 발생하면 예외가 아닌 null을 반환한다.
3. 디스패처
- 특정 코루틴을 실행할 때 사용할 스레드를 제어한다.
- context의 한 종류이기 때문에 코루틴 빌더 함수의 인자로 전달할 수 있다.
- Dispatchers.Default: 스레드 풀 기반이고, 풀 크기는 사용 가능한 CPU 코어 수 또는 2가 된다. (default)
- Dispatchers.IO: 스레드 풀 기반이고, I/O를 많이 사용하는 작업에 최적화되어 있다.
- Dispatchers.Main: 메인 스레드를 사용한다. UI와 상호작용하는 작업을 실행한다.
- Dispatchers.Unconfined: 현재 스레드에서 코루틴을 시작하지만, suspend 함수를 만나면 그 함수를 실행하는 스레드에서 수행된다.
- Dispatchers.Unconfined
fun main() = runBlocking {
launch(Dispatchers.Unconfined) {
println(Thread.currentThread().name) // main
delay(500)
println(Thread.currentThread().name) // default executor
}
}
-
- delay 함수는 default executor 스레드에서 실행된다.
- delay 함수를 호출한 후부터 코루틴의 스레드도 default executor로 변경된다.
fun main() {
// 스레드 풀 크기가 5고 스레드 이름이 WorkThread인 디스패처 생성
newFixedThreadPoolContext(5, "WorkThread").use { dispatcher ->
runBlocking {
for (i in 1..3) {
launch(dispatcher) {
println(Thread.currentThread().name)
delay(100)
}
}
}
}
}
- newFixedThreadPoolContext()를 통해 직접 만든 스레드 풀을 사용하는 디스패처를 만들 수 있다.
- newSingleThreadPoolContext()를 통해 스레드 하나만 사용하는 디스패처를 만들 수 있다.
fun CoroutineScope.getName() = Thread.currentThread().name
fun main() {
runBlocking {
println("Parent: ${getName()}") // main
launch {
println("Child, inherited: ${getName()}") // main
}
launch(Dispatchers.Default) {
println("Child, explicit: ${getName()}") // DefaultDispatcher-worker-1
}
}
}
- 최상위 코루틴에서부터 디스패처가 자동으로 상속된다.
- 최상위 코루틴에서 디스패처를 지정하지 않으면 Dispatchers.Default가 된다. (runBlocking() 빌더는 현재 스레드 사용)
4. 예외 처리
- 1) 예외를 부모 코루틴에게 전달하는 방법 (launch)
- 자식 코루틴에서 예외 발생
- 부모 코루틴이 똑같은 오류로 취소
- 부모의 나머지 자식도 모두 취소
- 부모는 예외를 자신의 부모로 전달
- 최상위 코루틴에 도달할 때까지 과정 반복
- 예외가 CoroutineExceptionHandler에 의해 처리
fun main() {
runBlocking {
launch {
throw Exception("Error in task 1")
println("Task 1 completed")
}
launch {
delay(1000)
println("Task 2 completed")
}
println("Root")
}
}
실행 결과
Root
Exception in thread "main" java.lang.Exception: Error in task 1
|
- 첫 번째 자식 코루틴은 예외를 던진다.
- 최상위 코루틴이 취소되고, 두 번째 자식 코루틴도 취소된다.
- CoroutineExceptionHandler()
- (CoroutineContext, Throwable) → Unit 타입인 람다를 인자로 받아 핸들러를 생성한다.
- 핸들러도 context이기 때문에 코루틴 빌더의 context 인자로 넘길 수 있다.
suspend fun main() {
val handler = CoroutineExceptionHandler{ _, exception ->
println("Caught $exception")
}
GlobalScope.launch(handler) {
launch {
throw Exception("Error in task 1")
println("Task 1 completed")
}
launch {
delay(1000)
println("Task 2 completed")
}
println("Root")
}.join()
}
실행 결과
Root
Caught java.lang.Exception: Error in task 1
|
-
- CoroutineExceptionHandler는 최상위 코루틴에서만 정의할 수 있다. → 예외를 부모로 전달하기 때문에
- runBlocking은 handler를 지정해도, 디폴트 핸들러를 사용하게 된다.
- 2) 발생한 예외를 저장해두기 (async)
- 발생한 예외를 저장해두고, 그 계산 결과를 받아오기 위해 await() 함수를 호출했을 때 예외가 다시 발생한다.
fun main() {
runBlocking {
val deferred1 = async {
throw Exception("Error in task 1")
println("Task 1 completed")
}
val deferred2 = async {
println("Task 2 completed")
}
deferred1.await() // 예외 발생
deferred2.await()
println("Root")
}
}
실행 결과
Exception in thread "main" java.lang.Exception: Error in task 1
|
- 첫 번째 자식 코루틴의 결과를 가져올 때 예외가 발생한다.
- async와 같은 빌더들은 CoroutineExceptionHandler를 사용하지 않기 때문에 context에 핸들러를 설정해도 디폴트 핸들러가 사용된다.
fun main() {
runBlocking {
val deferred1 = async {
throw Exception("Error in task 1")
println("Task 1 completed")
}
val deferred2 = async {
println("Task 2 completed")
}
try {
deferred1.await() // 예외 발생하면서 부모가 중단됨
deferred2.await()
} catch (e: Exception) {
println("Caught $e")
}
println("Root")
}
}
실행 결과
Caught java.lang.Exception: Error in task 1
Root
Exception in thread "main" java.lang.Exception: Error in task 1
|
- try-catch 블록으로 예외 처리를 시도해도 예외와 함께 중단된다.
- 부모를 취소시키기 위해 자동으로 다시 예외를 던지기 때문이다.
- supervisor job
fun main() {
runBlocking {
supervisorScope {
val deferred1 = async {
throw Exception("Error in task 1")
println("Task 1 completed")
}
val deferred2 = async {
println("Task 2 completed")
}
try {
deferred1.await() // 예외 발생하면서 부모가 중단되지 않음
deferred2.await()
} catch (e: Exception) {
println("Caught $e")
}
}
println("Root")
}
}
실행 결과
Task 2 completed
Caught java.lang.Exception: Error in task 1
Root
|
-
- supervisor job이 있으면 부모는 취소되지 않고 자신의 자식만 취소된다.
- supervisor job을 취소하면 자동으로 자신의 모든 자식을 취소한다.
- 코루틴을 supervisor job으로 변환하려면 supervisorScope() 함수를 사용한다.
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
supervisorScope {
val child = launch(handler) {
throw AssertionError()
}
child.join()
println("Scope finished")
}
}
실행 결과
Caught java.lang.AssertionError
Scope finished
|
- supervisor job에서는 자식 코루틴에서 핸들러를 설정할 수 있다.
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val parent = GlobalScope.launch(handler) {
val child = launch { throw IOException() }
try {
child.join()
} catch (e: CancellationException) {
println("Caught CancellationException")
}
}
parent.join()
}
실행 결과
Caught CancellationException
Caught java.io.IOException
|
- 취소 시 발생하는 CancellationException은 핸들러에게 무시된다.
- CancellationException이 발생하면 슈퍼바이저 잡과 동일하게 부모는 취소되지 않고 자식만 취소된다.
728x90
'kotlin' 카테고리의 다른 글
코틀린으로 코딩테스트 준비하기 (0) | 2023.10.18 |
---|---|
[kotlin/코틀린] 코루틴 동시성 통신 (채널, 프로듀서, 티커, 액터, 플로우) (0) | 2023.07.20 |
[kotlin/코틀린] 코루틴 기초 (suspend 함수, 코루틴 빌더, 문맥, 범위) (0) | 2023.07.19 |
[kotlin/코틀린] 위임 프로퍼티 (0) | 2023.07.19 |
[kotlin/코틀린] 연산자 오버로딩 (0) | 2023.07.19 |