iOS/Swift Concurrency

[스터디] Cancel 실험

Hamp 2025. 10. 22. 09:16
반응형

👋 들어가기 전

스터디에서, TaskGroup 관련된 내용으로 선생님께 너무 질문했더니, 선생님이 진이 빠지셨다.

 

그래서 한번 나혼자 마음대로 가지고 놀아보려고한다.

 

내가 예상한 내용과 실제 결과, 그리고 스터디에서 충격받은 내용을 함께 정리해보려고한다.


🏁 학습할 내용

  • Task가 중첩됐을 때
  • TaskGroup 만드는 함수를 감싼 Task를 캔슬했을 때
  • async let을 실행한 Task를 캔슬했을 때
  • Task.detached를 사용했을 때
  • TaskGroup 내에서,child가 에러를 throw하면 그룹 전체가 취소되는가?

 


0. Task가 중첩됐을 때

 

🧑‍💻 코드

func runTask() async throws {
    let task = Task {
        try await Task.sleep(for: .seconds(10))
        print("End Inner Task")
    }
}


let task = Task {
    do {
        try await runTask()
    } catch {
        print("runTask is Cancelled")
    }
}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    task.cancel()
    print("try cancel")
}

 

🧐 예상 동작

단순 중첩된 상태이므로, 상속관계가 없으므로 끝까지 실행될 듯(End Inner Task출력)

 

✅ 결과

try cancel
End Inner Task

 


1. TaskGroup 만드는 함수를 감싼 Task를 캔슬했을 때

 

🧑‍💻 코드

import Foundation

func runTaskGroup() async throws {
    try await withThrowingTaskGroup(of: (Int,Int).self, returning: [Int].self) { group in
        
        for i in 0..<10 {
            group.addTask {
                try await Task.sleep(for: .seconds(10-i))
                return (i, Int.random(in: 0..<100))
            }
        }
        
        var result: [Int] = []
        
        for try await (index, value) in group {
            print("🍯 \(index) \(value)")
            result.append(value)
        }
        
        
        return result
    }
}

let task = Task {
    do {
        try await runTaskGroup()
    } catch {
        print("runTaskGroup is Cancelled")
    }
}

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    print("try cancel")
    task.cancel()
}

RunLoop.main.run()

 

🧐 예상 동작

TaskGroup을 실행한 Task가 부모 Task고, 부모가 cancel을 불렀음

 

이 때, 비동기 반복문에 Task.CancellationError()가 호출되면서 cancel 될 듯

 

✅ 결과

🍯 9 72
🍯 8 96
try cancel
runTaskGroup is Cancelled

 


 2. async let을 실행한 Task를 캔슬했을 때

 

🧑‍💻 코드

import Foundation
let task1 = Task {
    async let child1: String = {
        for i in 0..<10 {
            try? Task.checkCancellation()
            try await Task.sleep(nanoseconds: 300_000_000)
            print("asyncLet child running \(i)")
        }
        return "done"
    }()

    
    do {
        let res = try await child1
        print("child1 result: \(res)")
    } catch {
        print("child1 error: \(error)")
    }
}

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    print("Canceled")
    task1.cancel()
}

RunLoop.main.run()

 

🧐 예상 동작

async let 을 실행한 Task가 부모 Task고, 부모가 cancel을 불렀음

 

이 때, 비동기 반복문에 Task.CancellationError()가 호출되면서 cancel 될 듯

 

✅ 결과

asyncLet child running 0
asyncLet child running 1
asyncLet child running 2
Canceled
child1 error: CancellationError()

3. Task.detached를 사용했을 때

 

🧑‍💻 코드

let task2 = Task {
    let detached = Task.detached {
         for i in 0..<10 {
             try await Task.sleep(nanoseconds: 200_000_000)
             print("detached running \(i)")
         }
         print("detached finished")
     }

     try await Task.sleep(nanoseconds: 350_000_000)

     // detached가 계속 도는지 관찰
     _ = await detached.result
     print("parent done")
}

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    print("Canceled")
    task2.cancel()
}

 

🧐 예상 동작

Task 중첩과 같은 구조, 실행도중에 바깥의 Task가 Cancel 되어도, 취소가 전파되지않고 끝까지 실행됨

 

✅ 결과

 detached running 0
 detached running 1
 detached running 2
 detached running 3
 Canceled
 detached running 4
 detached running 5
 detached running 6
 detached running 7
 detached running 8
 detached running 9
 detached finished
 parent done

4. TaskGroup 내에서,child가 에러를 throw하면 그룹 전체가 취소되는가?

 

🧑‍💻 코드

Task {
    do {
        try await withThrowingTaskGroup(of: Int.self) { group in
            group.addTask {
                try await Task.sleep(nanoseconds: 300_000_000)
                print("child A about to throw")
                throw NSError(domain: "Test", code: 1)
            }
            group.addTask {
                for i in 0..<10 {
                    try? Task.checkCancellation()
                    print("child B: \(i)")
                    try await Task.sleep(nanoseconds: 150_000_000)
                }
                return 10
            }
            // 그룹의 모든 child를 기다림
            while let value = try await group.next() {
                print("result: \(value)")
            }
        }
    } catch {
        print("group caught: \(error)")
    }
    print("after group")
}


RunLoop.main.run()

 

🧐 예상 동작

group.nextwhile을 통해 child 작업을 기다리다, NSError 발생 시 catch로 흐름이 꽂힘

 

✅ 결과

child B: 0
child B: 1
child A about to throw
child B: 2
group caught: Error Domain=Test Code=1 "(null)"
after group

5. withTaskCancellationHandler 동작

 

do-catch 문처럼, cancel 감지 시, clean up 작업할 수 있는 흐름을 받을 수 있음

 

🧑‍💻 코드

import Foundation


let task = Task {
    await withTaskCancellationHandler {
        print("operation start")
        try? await Task.sleep(nanoseconds: 300_000_000)
        print("operation end  Task.isCancelled = \(Task.isCancelled)")
    } onCancel: {
        print("cancellation handler executed")
    }

}

Task {
    try await Task.sleep(nanoseconds: 100_000_000)
    print("cancelling the handler task")
    task.cancel()
}


RunLoop.main.run()

 

🧐 예상 동작

cancel 감지 시, onCancel 쪽에 흐름이 꽂히고, operation 클로저 가장 마지막 print문

operation end .. 부분 호출 안됨

 

✅ 결과

예측 틀림, operation 클로저도 끝까지 실행됨, cancel에 따른 핸들링은 별도로 해야함

cancel 되면, onCancel 쪽 흐름이후, 다시 operation쪽으로 흐름

operation start
cancelling the handler task
cancellation handler executed
operation end  Task.isCancelled = true

출처

반응형