[스터디] Cancel 실험

👋 들어가기 전
스터디에서, 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.next와 while을 통해 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