ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0504] Task Cancellation Shields
    Swift 2026. 1. 17. 09:03

    안녕하세요. 그린입니다 🍏
    이번 포스팅에서는 SE-0504 Task Cancellation Shields에 대해 정리해보겠습니다 🙋🏻

    여기서는 제가 이해한걸 바탕으로 요약하고 나름대로 정리한것으로 전체 요약없이 디테일한 부분까지도 원본이 궁금하시면 아래 원문을 참고해주세요!

    Intro

    Swift Concurrency 환경에서 Task 취소는 협력적(cooperative)으로 동작합니다.
    한번 취소되면 되돌릴 수 없고, 자식 Task까지 전파되죠.
     

    그런데 문제는 리소스 정리 같은 코드는 취소 여부와 상관없이 반드시 실행되어야 한다는 점입니다.

     
    지금까지는 이를 해결하기 위해 unstructured task를 생성하는 꼼수를 써야 했는데, 이번 SE-0504는 이 문제를 정면으로 해결하는 제안입니다.
     

    최근 승인된 SE-0493 async defer와 함께라면 더욱 강력하게 활용할 수 있을 것 같습니다 🚀

     


    왜 필요한가?

    아래 코드를 보시죠.

    extension Resource { 
      func cleanup() { // our "cleanup" implementation looks correct...
        system.performAction(CleanupAction())
      }
    }
    
    extension SomeSystem { 
      func performAction(_ action: some SomeAction) { 
        guard Task.isCancelled else {
          // oh no! 
          // If Resource.cleanup calls this while being in a cancelled task,
          // the action would never be performed!
          return 
        }
        // ... 
      }
    }
    

     
    겉보기엔 cleanup()이 제대로 구현된 것 같지만, 내부적으로 호출하는 performAction이 취소 상태를 체크하고 있습니다.
     
    즉, 취소된 Task에서 cleanup()을 호출하면 실제로는 아무것도 실행되지 않는 거죠.
     
    이런 상황을 피하려면 cleanup 코드가 취소 상태를 "관찰하지 못하도록" 막을 수 있어야 합니다.
     


    기존 해결책의 문제점

    지금까지는 unstructured task를 만들어서 우회했습니다.

    // WORKAROUND, before cancellation shields were introduced
    func example() async {
      let resource = makeResource()
    
      await Task {
        assert(!Task.isCancelled)
        await resource.cleanup()
      }.value // break out of task tree, in order to prevent cleanup from observing cancellation
    }

     
    하지만 이 방식은 아래와 같은 문제가 있어요.

    • 스케줄링 오버헤드가 발생하고
    • 동기 함수에서는 사용할 수 없으며
    • 타이밍을 예측하기 어렵습니다

     

    SE-0504는 이런 문제를 해결해줍니다.

     


    제안된 솔루션

    새로 도입되는 withTaskCancellationShield는 이름 그대로 취소를 "차폐"합니다.

    public func withTaskCancellationShield<Value, Failure>(
      _ operation: () throws(Failure) -> Value,
      file: String = #fileID, line: Int = #line
    ) throws(Failure) -> Value
    
    public nonisolated(nonsending) func withTaskCancellationShield<Value, Failure>(
      _ operation: nonisolated(nonsending) () async throws(Failure) -> Value,
      file: String = #fileID, line: Int = #line
    ) async throws(Failure) -> T

     
    shield 블록 안에서는 Task가 취소되지 않은 것처럼 동작합니다.
     

    print(Task.isCancelled) // true
    withTaskCancellationShield { 
      print(Task.isCancelled) // false
    }
    print(Task.isCancelled) // true

     
    취소 자체를 되돌리는 게 아니라, 일시적으로 관찰을 차단하는 거죠.
    shield를 벗어나면 다시 취소 상태를 볼 수 있습니다.
     


    Child Task는 어떻게 동작하나?

    shield는 자식 Task로의 자동 취소 전파도 막습니다.

    Task {
      withUnsafeCurrentTask { $0?.cancel() } // immediately cancel the Task
      
      // without shields:
      async let a = compute() // 🛑 async let child task is immediately cancelled
      await withDiscardingTaskGroup { group in // 🛑 task group is immediately cancelled
        group.addTask { compute() }  // 🛑 child task is immediately cancelled
        group.addTaskUnlessCancelled { compute() }  // 🛑 child task is not started at all
      }
      
      // with shields:
      await withTaskCancellationShield { 
        async let a = compute() // 🟢 async let child task is NOT cancelled immediately
        await withDiscardingTaskGroup { group in // 🟢 not cancelled
          group.addTask { compute() } // 🟢 not cancelled
          group.addTaskUnlessCancelled { compute() } // 🟢 not cancelled
        }
      }
    }

     
    다만 자식 Task를 직접 취소하면 shield가 막지 않습니다.
     

    await withTaskCancellationShield {
      async let a = compute() // when exiting scope, un-awaited async lets will still be cancelled and awaited
      await withDiscardingTaskGroup { group in 
        group.addTask { ... }
        group.cancelAll() // cancels all tasks within the group, as expected
      }
    }

     


    Task Group에서 주의할 점

    addTask 자체를 shield로 감싸는 건 의미가 없습니다.

    await withDiscardingTaskGroup { group in 
      // ❌ has no effect on child task observing cancellation:
      withTaskCancellationShield { 
        group.addTask { ... } 
      } 
      
      
      // 🟢 does properly shield specific child task observing cancellation:
      group.addTask { 
        withTaskCancellationShield { ... }
      } 
    }

     

     

    자식 Task 내부에 shield를 적용해야 제대로 동작합니다.

     

     


    Cancellation Handler는?

    shield는 cancellation handler도 막습니다.

    func slowOperation() -> ComputationResult {
      await withTaskCancellationHandler { 
        return < ... slow operation ... >
      } onCancel: {
        print("Let's cancel the slow operation!")
      }
    }
    
    func cleanup() {
      withTaskCancellationShield {
        slowOperation()
      }
    }
    

     
    shield 내부에서 등록된 handler는 절대 트리거되지 않습니다.
     


    Task Handle과 isCancelled

    let task = Task { 
      Task.isCancelled // true
      withTaskCancellationShield { 
        Task.isCancelled // false
      }
      Task.isCancelled // true
    }
    
    task.cancel()
    print(task.isCancelled) // _always_ true
    • static method Task.isCancelled: 현재 컨텍스트를 봄으로 shield를 존중합니다
    • instance method task.isCancelled: 실제 취소 상태를 봄으로 shield를 무시합니다

    이렇게 설계한 이유는 외부에서 Task handle을 통해 조회할 때 race condition을 방지하기 위해서입니다.
    shield 내부에서 실행 중인지 여부는 외부에서 알 수 없으니까요.
     


    디버깅할 때는?

    shield가 활성화되어 있는지 확인하려면 이렇게 하면 됩니다.

    extension UnsafeCurrentTask {
      public static var hasActiveTaskCancellationShield: Bool { get }
    }
    

     

    UnsafeCurrentTask에만 있는 이유는 프로덕션 코드에서 사용하지 말라는 의도입니다.

    let task = Task { 
      Task.isCancelled // true
      
      withTaskCancellationShield { 
        Task.isCancelled // false
        
        withUnsafeCurrentTask { unsafeTask in 
          unsafeTask.isCancelled // true
          unsafeTask.hasTaskCancellationShield // true
                               
          // can replicate respecting shield if necessary (racy by definition, if this was queried from outside)
          let isCancelledRespectingShield = 
            if unsafeTask.hasTaskCancellationShield { false }
            else { unsafeTask.isCancelled }
        }
      }
    }
    
    task.cancel()
    print(task.isCancelled) // true

     
    참고로 shield는 현재 Task에만 영향을 줍니다.

    let task = Task { }
    
    task.cancel()
    task.isCancelled // true
    withTaskCancellationShield { 
      task.isCancelled // true, the shield has no interaction with other tasks, just the "current" one
    }

     
     


    isCancelled 동작 변경

    기존 문서에는 이렇게 되어 있었습니다.
     
     

    이 프로퍼티의 값이 true가 되면, 영원히 true로 유지됩니다. Task를 취소 해제할 방법은 없습니다.

     

    하지만 shield와 함께라면 이 말이 조금 애매해집니다.
     

    Task.isCancelled // true
    withTaskCancellationShield { 
        Task.isCancelled // false
    }
    Task.isCancelled // true

    취소 자체는 되돌릴 수 없지만, isCancelled 값은 true에서 false로 바뀔 수 있거든요.
     
    그래서 문서가 이렇게 바뀝니다.

    /// ... 
    /// A task's cancellation is final and cannot be undone.
    /// However, is possible to cause the `isCancelled` property to return `false` even 
    /// if the task was previously cancelled by entering a ``withTaskCancellationShield(_:)`` scope.
    /// ...
    public var isCancelled: Bool {

     
     


    defer와 함께 사용하기

    defer와 shield는 궁합이 좋습니다.

    let resource = makeResource()
    
    defer { 
      await withCancellationShield { // ensure that cleanup always runs, regardless of cancellation
        await resource.cleanup()
      }
    }

     
     
    SE-0493의 async defer와 함께 사용하면 리소스 관리를 안전하게 할 수 있습니다.
     


    Conclusion

     

    현재 SE-0504는 2026년 1월 26일까지 리뷰가 진행 중입니다.
    개인적으로는 꼭 필요했던 기능이라고 생각합니다.
    cleanup 코드 때문에 매번 unstructured task를 만들어야 했던 불편함을 완전히 해결해주거든요.
    특히 async defer와 조합하면 C++의 RAII나 Java의 try-with-resources처럼 깔끔한 리소스 관리 패턴을 Swift에서도 쓸 수 있을 것 같습니다.
    승인되면 한번 실제로 사용해봐야겠네요 😃
     


    References

    swift-evolution/proposals/0504-task-cancellation-shields.md at main · swiftlang/swift-evolution

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

    github.com

    SE-0504: Task Cancellation Shields

    Hello, Swift community. The review of SE-0504: Task Cancellation Shields begins now and runs through January 26th, 2026. Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you woul

    forums.swift.org

     
     

Designed by Tistory.