코루틴을 어떻게 취소할 수 있는지 살펴보자!
Job 인터페이스는 cancel 메서드를 제공suspend fun main(): Unit = coroutineScope {
val job = launch {
repeat(1000) { i ->
delay(200)
println("Printing $i")
}
}
delay(1100)
job.cancel()
job.join()
println("Cancelled successfully")
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// Cancelled successfully
// join을 호출하지 않는 경우
suspend fun main(): Unit = coroutineScope {
val job = launch {
repeat(1000) { i ->
delay(100)
Thread.sleep(100) // 오래 걸리는 연산이라 가정
println("Printing $i")
}
}
delay(1000)
job.cancel()
println("Cancelled successfully")
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Cancelled successfully
// Printing 4
// join을 호출하는 경우
suspend fun main(): Unit = coroutineScope {
val job = launch {
repeat(1000) { i ->
delay(100)
Thread.sleep(100) // 오래 걸리는 연산이라 가정
println("Printing $i")
}
}
delay(1000)
job.cancel()
job.join()
println("Cancelled successfully")
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// Cancelled successfully
cancel과 join을 함께 호출할 수 있는 cancelAndJoin 확장함수를 사용할 수 있음Job() 팩토리 함수로 생성된 잡도 같은 방법으로 취소될 수 있으며, 잡에 딸린 많은 코루틴을 한번에 취소할 때 사용할 수 있음suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
repeat(1000) { i ->
delay(100)
println("Printing $i")
}
}
delay(1100)
job.cancelAndJoin()
println("Cancelled successfully")
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// Cancelled successfully
class ProfileVievModel : ViewModel() {
private val scope =
CoroutineScope(Dispatchers.Main + SupervisorJob())
fun onCreate() {
scope.launch { loadUserData() }
}
override fun onCleared() {
scope.coroutineContext.cancelChildren()
}
// ...
}
CancellationException 발생suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
try {
repeat(1000) { i ->
delay(100)
println("Printing $i")
}
} catch (e: CancellationException) {
println(e)
throw e
}
}
delay(1100)
job.cancelAndJoin()
println("Cancelled successfully")
delay(1000)
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// JobCancellationException ...
// Cancelled successfully
finally 블록 활용suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
try {
delay(Random.nextLong(2000))
println("Done")
} finally {
println("Will always be printed")
}
}
delay(1000)
job.cancelAndJoin()
}
// Will always be printed
// (또는)
// Done
// Will always be printed
Job은 이미 ‘Cancelling’ 상태가 되었기 때문에 중단되거나 다른 코루틴을 시작하는 건 절대 불가능CancellationException을 던짐suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
try {
delay(2000)
println("Job is done")
} finally {
println("Finally")
launch { // 무시됨
println("Will not be printed")
}
delay(1000) // 예외 발생 지점
println("Will not be printed")
}
}
delay(1000)
job.cancelAndJoin()
println("Cancel done")
}
// (1초 후)
// Finally
// Cancel done
withContext(NonCancellable)로 포장하여 사용Job인 NonCancellable 객체를 사용suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
try {
delay(2000)
println("Coroutine finished")
} finally {
println("Finally")
withContext(NonCancellable) {
delay(1000L)
println("Cleanup done")
}
}
}
delay(100)
job.cancelAndJoin()
println("Done")
}
// Finally
// Cleanup done
// Done
Job의 invokeOnCompletion 메서드를 호출하는 것invokeOnCompletion 메서드는 잡이 ‘Completed’나 ‘Cancelled’와 같은 마지막 상태에 도달했을 때 호출될 핸들러를 지정suspend fun main(): Unit = coroutineScope {
val job = launch {
delay(1000)
}
job.invokeOnCompletion { exception: Throwable? ->
println("Finished")
}
delay(400)
job.cancelAndJoin()
}
// Finished
nullCancellationExceptioninvokeOnCompletion이 호출되기 전에 완료되었으면 핸들러는 즉시 호출됨onCancelling과 invokeImmediately 파라미터를 사용하면 핸들러의 동작방식을 변경할 수 있음suspend fun main(): Unit = coroutineScope {
val job = launch {
delay(Random.nextLong(2400))
println("Finished")
}
delay(800)
job.invokeOnCompletion { exception: Throwable? ->
println("Will always be printed")
println("The exception was: $exception")
}
delay(800)
job.cancelAndJoin()
}
// Will always be printed
// The exception was:
// kotlinx.coroutines.JobCancellationException
// (또는)
// Finished
// Will always be printed
// The exception was null
// 예) 취소불가능한 경우
suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
repeat(1000) { i ->
Thread.sleep(200) // 여기서 복잡한 연산이나 IO 작업이 있다고 가정
println("Printing $i")
}
}
delay(1000)
job.cancelAndJoin()
println("Cancelled successfully")
delay(1000)
}
// Printing 0
// Printing 1
// Printing 2
// ... (1000까지)
yield()를 주기적으로 호출yield는 코루틴을 중단하고 즉시 재실행suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
repeat(1000) { i ->
Thread.sleep(200)
yield()
println("Printing $i")
}
}
delay(1000)
job.cancelAndJoin()
println("Cancelled successfully")
delay(1000)
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// Cancelled successfull
CoroutineScope는 coroutineContext 프로퍼티를 사용해 참조할 수 있는 컨텍스트를 가지고 있음public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
do {
Thread.sleep(200)
println("Printing")
} while (isActive)
}
delay(1100)
job.cancelAndJoin()
println("Cancelled successfully")
}
// Printing
// Printing
// Printing
// Printing
// Printing
// Printing
// Cancelled successfully
// CancellationException을 던지는 ensureActive() 함수 사용 방식
suspend fun main(): Unit = coroutineScope {
val job = Job()
launch(job) {
repeat(1000) { i ->
Thread.sleep(200)
ensureActive()
println("Printing $i")
}
}
delay(1100)
job.cancelAndJoin()
println("Cancelled successfully")
}
// Printing 0
// Printing 1
// Printing 2
// Printing 3
// Printing 4
// Cancelled successfully
ensureActive() 함수는 CoroutineScope에서 호출되어야 함yield 함수는 최상위 중단 함수suspendCancellableCoroutine를 사용하면 CancellableContinuation<T>를 통해 취소 시점에 자원 해제 가능invokeOnCancellation 콜백으로 취소 시 추가 처리 가능suspend fun someTask() = suspendCancellableCoroutine { cont ->
cont.invokeOnCancellation {
// 정리 작업 수행
}
// 나머지 구현 부분
}
// Retrofit의 Call 함수도 중단 함수로 래핑되어 있음
suspend fun getOrganizationRepos(
organization: String
): List<Repo> = suspendCancellableCoroutine { continuation ->
val orgReposCall = apiService
.getOrganizationRepos(organization)
orgReposCall.enqueue(object : Callback<List<Repo>> {
override fun onResponse(
call: Call<List<Repo>>,
response: Response<List<Repo>>
){
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
continuation.resume(body)
} else {
continuation.resumeWithException(ResponseWithEmptyBody)
}
} else {
continuation.resumeWithException(
ApiException(
response.code(),
response.message()
)
)
}
}
override fun onFailure(
call: Call<List<Repo>>,
t: Throwable
){
continuation.resumeWithException(t)
}
})
continuation.invokeOnCancellation {
orgReposCall.cancel()
}
}