😺

【Swift Concurrency】withTaskCancellationHandlerについて

2024/06/23に公開

概要

Swift Concurrencyにおけるキャンセル時の処理は少々複雑だとは思いますが、そんな中withTaskCancellationHandlerは割と手軽にキャンセル時の処理を行うことができます。

@backDeployed(before: macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4)
func withTaskCancellationHandler<T>(
    operation: () async throws -> T,
    onCancel handler: () -> Void
) async rethrows -> T

operationの実行中にキャンセルが発生した場合、onCancelを呼び出し、キャンセル時の処理を行うことができます。以下のコードではTaskを定数taskに保持し、1秒後にキャンセルしています。

Task {
   let task = Task {
        await withTaskCancellationHandler {
            let data = await fetchImageData()
            print(data as Any)
        } onCancel: {
            print("called onCancel Closure")
        }
    }

    try? await Task.sleep(for: .seconds(1))

    task.cancel()
}

// 4Kの画像を取得するため、多くの場合1秒以上かかります。
func fetchImageData() async -> Data? {
    guard let url = URL(string: "https://picsum.photos/3840/2160"),
          let (data, _) = try? await URLSession.shared.data(from: url) else {
              return nil
          }

    return data
}

出力は以下の通りです。

called onCancel Closure
nil

fetchImageDataの実行中にtaskがキャンセルされ、onCancelが呼ばれます。その後にスコープを抜ける処理などは記述していないため、print(data as Any)も呼ばれ、nilが出力されています。

注意点

withTaskCancellationHandlerは、すでにキャンセルされたタスク内でも呼び出される可能性があります。そのような場合でも、withTaskCancellationHandlerは実行されます。この場合、onCancelが実行され、その後operationが実行されることに注意が必要です。

以下のコードは、キャンセルされたタスク内でwithTaskCancellationHandlerを呼び出したサンプルコードです。

Task {
    let task = Task {
        try? await Task.sleep(for: .seconds(1))

        print("Task.isCancelled:", Task.isCancelled)
        await withTaskCancellationHandler {
            print("called operation closure")
        } onCancel: {
            print("called onCancel closure")
        }
    }

    task.cancel()
}

出力結果は以下の通りです。

Task.isCancelled: true
called onCancel closure
called operation closure

出力結果から分かる通り、すでにキャンセルされているタスクではonCancelが呼び出され、その後にoperationが呼び出されていることが確認できます。

※ 最初の出力はきちんとタスクがキャンセルされているか確認するためのものです。

公式ドキュメント

https://developer.apple.com/documentation/swift/withtaskcancellationhandler(operation:oncancel:)

Discussion