Android 공부/Coroutine

[XX캠퍼스] 03. Kotlin Coroutines & Flow ( 취소와 타임아웃 )

Machine_웅 2022. 7. 21. 15:12
728x90
반응형

Job에 대해 취소

명시적인 Job에 대해 cancel 메서드를 호출해 취소할 수 있습니다.

( * Join 은 어떤 행동이 끈날때까지 대기하는 것 )

 

import kotlinx.coroutines.*

suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }

    val job2 = launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }

    val job3 = launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)
        println("2!")  
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

실행결과 
launch1: main @coroutine#2
launch2: main @coroutine#3
1!
launch3: main @coroutine#4
2!
4!
runBlocking: main @coroutine#1
5!

 

* cancel() 을 통해서 취소를 할 수 있는데, 그전에  Job을 받아와야 한다. 


 

취소 불가능한 Job

launch(Dispatchers.Default)는 그 다음 코드 블록을 다른 스레드에서 수행을 시킬 것입니다.

나중에 자세히 알아볼테니 지금은 넘어갑시다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancel()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

수행결과 
1
doCount Done!
2
3
4
5
6
7
8
9
10

* 취소를 못한이유 - job을 통해서 cancel 할수 있게 이해?를 못시켰다.

 

 

두가지 부분이 신경이 쓰입니다.

  1. job1이 취소든 종료든 다 끝난 이후에 doCount Done!을 출력하고 싶다.
  2. 취소가 되지 않았다.

먼저 취소든 종료든 다 끝난 이후에 doCount Done!을 출력합시다.

 

=>  위의 문제를 해결하기 위해서는 cancel과 join 을 사용해야 한다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancel()
    job1.join()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

실행결과
1
2
3
4
5
6
7
8
9
10
doCount Done!

cancel 이후에 join을 넣어서 실제로 doCount가 끝날 때 doCount Done!가 출력하게 했습니다.

= > 캔슬을 시도후, 모두 종료 될 때까지 기다리기 위해 join을 사용 . 

 

* 하지만 캔슬은 시키지 못했다.  

 

 

cancelAndJoin

cancel을 하고 join을 하는 일은 자주 일어나는 일이기 때문에

한번에 하는 cancelAndJoin이 준비되어 있습니다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

결과는 cancel을 하고 join을 하는 것과 동일. 

 

그럼 실제로 취소할라면 우째야해 ??

 


cancel 가능한 코루틴 isActive

isActive를 호출하면 해당 코루틴이 여전히 활성화된지 확인할 수 있습니다.

 isActive를 루프에 추가해봅시다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
    
    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

실행결과 
1
doCount Done!

isActive 를 활용해서  더이상 수행 할수 있을때만 일을 하도록 처리 . 

 


finally를 같이 사용( 작업취소후 자원 해제 )

-사용 예시 ( file , socket

launch에서 자원을 할당한 경우에는 어떻게 정리해야할까요?

 

suspend 함수들은 JobCancellationException를 발생하기 때문에

표준 try catch finally로 대응할 수 있습니다.

 

import kotlinx.coroutines.*

suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        try {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        } finally {
            println("job1 is finishing!")
            // 파일을 닫아두는 코드 작성
        }
    }

    val job2 = launch {
        try {
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        } finally {
            println("job2 is finishing!")
            // 소켓을 닫아두는 코드 작성.
        }
    }

    val job3 = launch {
        try {
            println("launch3: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        } finally {
            println("job3 is finishing!")
            // 파일을 닫아두는 코드 작성
            // 소켓을 닫아두는 코드 작성.
        }
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

catch에 예외발생시 처리 하도록 한다. 
위의 예제에서는 finally에서 했다. 

 


취소 불가능한 블록

어떤 코드는 취소가 불가능해야 합니다. 

withContext(NonCancellable)을 이용하면 취소 불가능한 블록을 만들 수 있습니다.

import kotlinx.coroutines.*

suspend fun doOneTwoThree() = coroutineScope {
    val job1 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }
        delay(1000L)
        print("job1: end")
    }

    val job2 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
        delay(1000L)
        print("job2: end")
    }

    val job3 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        }
        delay(1000L)
        print("job3: end")
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThree()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}


실행 결과 
launch1: main @coroutine#2
launch1: main @coroutine#3
launch1: main @coroutine#4
4!
3!
1!
2!
runBlocking: main @coroutine#1
5!

취소 불가능한 코드를 finally절에 사용할 수도 있습니다.

finally중에도 취소될수 있기때문에 withContext(NonCancellable)을 이용.

 


타임 아웃

일정 시간이 끝난 후에 종료하고 싶다면 withTimeout을 이용할 수 있습니다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    withTimeout(500L) {
        doCount()
    }
}

실행결과 
      doCount()
    }
}
1
2
3
4
Exception in thread "main" kotlinx.coroutines.
TimeoutCancellationException: Timed out waiting for 500 m

취소가 되면 TimeoutCancellationException 예외가 발생합니다.

 

 

withTimeoutOrNull

예외를 핸들하는 것은 귀찮은 일입니다.

withTimeoutOrNull을 이용해 타임 아웃할 때 null을 반환하게 할 수 있습니다.

 

import kotlinx.coroutines.*

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    val result = withTimeoutOrNull(500L) {
        doCount()
        true
    } ?: false
    println(result)
}

실행결과
1
2
3
4
false

 

성공할 경우 whithTimeoutOrNull의 마지막에서

 true를 리턴하게 하고 실패했을 경우 null을 반환할테니

엘비스 연산자(?:)를 이용해 false를 리턴하게 했습니다.

 

엘비스 연산자는 null 값인 경우에 다른 값으로 치환합니다.

코틀린의 예외는 식(expression)이어 활용이 어렵지는 않습니다만

개인적으로는 null을 리턴하고 엘비스 연산자로 다루는게 더 편한 것 같습니다.

728x90
반응형