5일차 주제: Cancellation propagation

Task cancellation
오늘은 부엌에서 요리사들이 스프를 만드는 상황을 가정해보겠습니다.
각 요리사들은, 스프를 만들기 위해서
- 재료 손질
- 닭고기 재우기
- 국물 끓이기
세 가지의 과정을 거치는데요. 이를 계층 구조로 살펴보면 어떨까요?
위와 같이, 부모 Task인 makeSoup라는 스프 만들기 내에는 세 가지의 작업들이 존재하고,
그 중에서도 재료 손질(chopIngredient)은 손질해야 하는 재료의 개수만큼
자식(chop)을 생성하고 있어요.
이러한 계층 구조를 가지고 있을 때의 장점은 무엇일까요?
만약, 수프를 만들던 도중, 이를 중단하고 싶다고 가정해봅시다.
task.cancel()
부모 Task에 cancel()을 하면, 부모의 아래로 존재하는 자식들은 모두 취소 신호를 전달받는데요.
이를 취소가 전파된다고 말합니다.
이전 시간에 잠시 언급했지만, 취소 신호를 전달받았다고 해서 작업이 멈추는 것은 아닙니다.
실제로 취소가 전파되었을 때, 작업을 멈추게 하려면
- 해당 신호를 받았을 경우, 취소 에러를 던지는 등의 명시적인 처리
를 해주어야 되는데요. (취소의 cooperative한 특성)
작업 취소를 직접적으로 처리하는 방법에는 두 가지가 있습니다.
1. Task.isCancelled
- pot 끓이는 작업을 비동기로 시작하고,
- Task.isCancelled를 확인하여 취소 상태이면 SoupCancellationError를 던집니다.
- 아니면 나머지 작업들(chop, marinate, cook)을 계속 진행합니다.
위 코드는 makeSoup에서 isCacelled를 통해 취소 검사를 추가한 모습인데요.
만약 makeSoup가 시작되면, guard 문으로 해당 작업이 cancel 상태인지 검사합니다.
guard문에서 작업이 취소된 상태라면, SoupCancellationError를 발생시키고
아래의 작업을 실행하지 않게 됩니다.
2. try Task.checkCancellation()
isCacelled는 취소 상태를 나타내는 불리언 값을 확인해서, Error를 직접 던지는 형태였습니다.
이외에도 취소가 되었을 경우 바로 Error를 던지는 checkCancellation()도 있습니다.
그런데요…! 위 두 사례에서
- isCancelled → guard문 통과
- Task.checkCacellation → 에러 x
와 같이, 함수의 진입점에서 취소 여부를 확인했을 때, 취소되지 않았다고 가정해봅시다.
검사를 무사히 통과되었기 때문에 아래쪽에서 자식 Task들이 진행될텐데요.
이때, 자식 Task들이 진행될 동안 cancel이 호출된다면 어떨까요?
코드들을 살펴보면, 맨 처음 진입할 때만 한 번 취소 검사를 하고
그 이후에는 존재하지 않기 때문에,
아래쪽이 진행되는 동안에는 아무런 취소를 감지하지 못하고 처리를 못할 것입니다.
위 두 가지 방식은, 취소 상태가 바뀌었는지 주기적으로 확인이 필요한 polling 방식이에요.
때문에 모든 함수를 실행하기 전에, 취소 여부를 확인해서 ”해당 작업이 필요한지?” 확인하는 것이 안전합니다.
3. withTaskCancellationHandler
하지만 isCancelled나 checkCancellation 두 폴링 방식이 부족할 수도 있습니다.
주기적으로 개발자가 취소되었는지? 여부를 확인해보는 것이 아닌,
작업이 취소되었을 때의 이벤트를 받을 경우 핸들링을 할 수 있는 Event-driven 방식도 있는데요.
바로 withTaskCancellationHandler 입니다.
핸들링 방식은 언제 유용할까요? 예시를 살펴보겠습니다.
AsyncSequence
- 비동기로 값을 하나씩 순서대로 받을 수 있는 타입
- 예시) 2일차에서 살펴보았던 bytes() 함수: 비동기적으로 서버의 값을 한줄씩 읽는 API
다음과 같은 상황이 있다고 가정해보겠습니다.
- 요리사는 주문이 들어오는 대로 수프를 만드는데요.
- 주문이 들어오지 않을 경우, 요리사는 (한가하게) 주문이 들어오기를 기다려요.
이 때, 교대 시간이 되어 요리사가 작업을 중단하고자 하는데요.
만약 주문을 받아, makeSoup를 실행하고 있는 중이라면 취소를 감지할 수 있습니다.
(makeSoup 내부에서 취소 신호를 받아 처리해 줄 수 있기 때문입니다.)
하지만, 단순히 주문을 기다리고 있는 상황이라면, 취소를 감지할 수 있을까요?
다음 주문을 기다리기 위해서 await, 중단 상태가 되었기 때문에,
취소를 감지할 수가 없습니다.
이때 AsyncSequences는 withTaskCancellationHandler 를 사용해 취소를 감지합니다.
위 함수는, AsyncSequence에서 다음 값을 비동기적으로 내보내주는 next() 함수인데요.
여기에서 onCancel 핸들러에서 취소 신호를 캐치하면,
내부에서 state를 cancel하는 명령을 즉시 실행합니다.
이렇게 cancel을 감지하면, state.isRunning이 false가 되어 시퀀스가 종료되겠쬬
구조적 동시성과 작업의 취소
Structured Concurrency를 만드는 두 가지 방식이 있었습니다.
- async let
- TaskGroup
1. async let
우선, async let에서의 취소 전파를 살펴보겠습니다.
cancel()을 통해 async let들의 상위 task가 취소된 경우는, 부모-자식까지 순서대로 취소가 전파됩니다.
반대로 자식들 중 한 개가 에러를 던진다면,
func cook() async throws {
async let a = try burnSomething() // 에러가 발생할 경우
async let b = try safeWork() // 정상 진행 중
// 여기에서 실제 에러가 밖으로 나가려는 시점
let _ = try await (a, b) // 이 때 b도 cancel하고, 모두 정리한 후 throw
}
try await으로 에러를 잡는 시점에서, 해당 에러를 인지하고
비로소 다른 자식들에게 취소가 전파됩니다.
2. TaskGroup
TaskGroup의 경우, 상위 Task에서 cancel() 요청을 받으면, 부모 작업에 취소가 전파됩니다.
부모 작업은 취소를 전파받을 경우, cancelAll() 함수를 실행해서,
자식 작업들 모두에게 취소를 전파할 수 있게 돼요.
반대로 자식 작업들 중 하나가 에러를 던질 경우에는 어떨까요?
withTaskGroup { ... } ❌ 자식이 throw 못 함
withThrowingTaskGroup { ... } ✅ 자식이 throw 가능
만약 withThrowingTaskGroup 내에서 자식 중 하나가 에러를 던질 경우,
부모는 이를 받아서 밖으로 에러를 던질 수 있습니다.
이때 중요한 것은, 부모가 해당 자식이 던진 에러를 받아야만
비로소 모든 자식을 취소할 수 있다는 것입니다.
🤔 무슨 소리인가요 …?
아래의 예시를 살펴보도록 하겠습니다.
- 세 개의 자식 Task를 추가
- 자식들을 모두 취소함 (cancelAll)
- 그럼에도 불구하고, 모든 자식 Task가 출력됨
그 이유는, cancelAll을 하더라도 단순 취소만 전달되었을 뿐,
각 task 내에서 취소 여부를 확인해서 error를 던지지 않으면 catch로 넘어가지 않는 것입니다.
즉, 위와 같이 에러를 감지해주는 수단이 꼭 필요합니다!
- 자식 중 하나가 throw
- 부모 TaskGroup이 이를 감지
- 비로소 다른 자식들에게 취소가 전파
TaskGroup의 기타 활용법
Task Group을 사용할 때, 동시에 실행되는 작업의 수를 제한할 수도 있습니다.
예를 들어서, VIP 주문이 많아져서 수프를 빨리 만들고 있는데,
재료를 써는 작업이 많아졌는데 도마의 수는 한정되어 있다고 가정해봅시다.
한꺼번에 재료를 너무 많이 썰면 다른 일을 할 공간이 부족하잖아요?
그래서, 한 번에 동시에 썰 수 있는 재료의 수를 제한해야 하는데요.
그래서 위와 같이, 최대로 병렬 처리할 수 있는 개수를 지정하고
해당 개수만큼 자식을 생성하다가,
각 작업이 끝날 때마다 동적으로 다음 작업을 추가하게 할 수 있습니다.
- 0, 1, 2 인덱스의 작업만 먼저 실행합니다.
- 그 결과는 choppedIngredient에 append됩니다.
- 작업의 결과를 하나씩 받을 때마다, addTask를 진행합니다.
- 해당 결과값도 choppedIngredient에 append 됩니다.
참고 자료
https://developer.apple.com/kr/videos/play/wwdc2023/10170/
구조화된 동시성의 기초를 넘어 - WWDC23 - 비디오 - Apple Developer
핵심은 작업 트리에 있습니다. 앱이 자동 작업 취소, 작업 우선순위 전파, 유용한 작업 로컬 값 패턴을 관리하는 데 구조화된 동시성이 어떻게 도움이 되는지 알아보세요. 유용한 패턴과 최신 작
developer.apple.com
https://www.hackingwithswift.com/quick-start/concurrency/how-to-cancel-a-task-group
How to cancel a task group - a free Swift Concurrency by Example tutorial
Was this page useful? Let us know! 1 2 3 4 5
www.hackingwithswift.com
'콩코롱시의 밤' 카테고리의 다른 글
[Swift concurrency] 콩코롱시의 밤 6일차 (4) | 2025.07.01 |
---|---|
[Swift concurrency] 콩코롱시의 밤 4일차 (3) | 2025.06.20 |
[Swift concurrency] 콩코롱시의 밤 3일차 (2) | 2025.06.20 |
[Swift concurrency] 콩코롱시의 밤 2일차 (0) | 2025.06.20 |
[Swift concurrency] 콩코롱시의 밤 1일차 (5) | 2025.06.20 |