😬

[Swift]WithCheckedContinuationが導入された背景

2023/08/20に公開

概要

Swift Evolutionを読んで withUnsafeContinuationwithCheckedContinuationが導入された背景をまとめる。
withUnsafeContinuationwithCheckedContinuationは、従来のcompletion handler記法を、新しいasync/awaitと連携させるために導入された。

背景

Async/Await構文は、既存のCompletion Handlerで記述された非同期処理と連携できる必要がある。
Async/Awaitでは、処理を"中断"と"再開"でき、再開に必要な情報は、"継続(Continuation)"として保存される。
一方、Completion Handlerでは、"中断"と"再開"のしくみが異なるため、そのままでは連携されることができない。
async/awaitでは"継続オブジェクト"を使用して処理の"中断"と"再開"を可能にしている一方で、Completion Handlerでは、"継続オブジェクト"が作られない。
Completion Handlerで記述された非同期処理のコードを、async/awaitと連携させるために、with(Un)safeContinuationが提案された。

withUnsafeContinuationとwithCheckedContinuation

Completion Handlerでの非同期処理からでも、"継続オブジェクト"を取得できるAPIが提案された。
新しいAPIを使うことで、Completion Handlerによる非同期処理からでも、"継続オブジェクト"を使用した、処理の中断や再開が行えるようになった。
2つのAPIが新たに提供された

  • withUnsafeContinuation
func withUnsafeContinuation<T>(_ fn: (UnsafeContinuation<T, Never>) -> Void) async -> T
  • withCheckedContinuation
func withCheckedContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Never>) -> Void
) async -> T

どちらも使い方は同じで、引数として継続を引数として受けとる処理(body)を渡す必要がある。

withCheckedContinuation { cotinuation in
    hogehoge
}    

クロージャでは、継続オブジェクト(Continuation)にアクセスでき、非同期処理の完了後に、"継続"を"再開"する必要がある。

挙動

  • bodyはすぐに実行される
  • bodyがreturnしたタイミングで処理はSuspendされる
  • body内の非同期処理が終わったタイミングでcontinuation.resume()を呼ぶ必要がある
  • resumeされたあと、継続はすぐに実行されるわけではなく、システムがスケジュールしたときに実行される

例を用いて説明すると

withCheckedContinuationを使った例

//Completion Handlerを使った非同期処理
func method1(completion: @escaping (String)->Void) {
    DispatchQueue.main.async {
        completion("Success")
    }
}

// withCheckedContinuationを使用して、async/awaitに書き換える
func method_async_checked() async -> String {
    await withCheckedContinuation { continuation in //以下が`body`の部分
        // 1. `method1`はすぐに実行される
        method1 { str in
            // 3. 非同期処理が終わったタイミングで `resume()`を呼ぶ。
            // 4. `継続`はシステムにスケジュールされたとき、実行される。
            continuation.resume(returning: str)
        }

        // 2. `return`のタイミングで "中断状態"になる
        return
    }
}

let result = await method_async_checked()
print(result)
  1. method1はすぐに実行される
  2. returnのタイミングで "中断状態"になる
  3. 非同期処理が終わったタイミングで resume()を呼ぶ。
  4. 継続はシステムにスケジュールされたとき、実行される。(ここでは、継続はprint(result)以降の処理)

規則

  • continuation.resume()ちょうど1回呼ばれないといけない
    • .resumeを複数回呼んだときの動作は、未定義となっている
    • .resumeを一度も呼ばないと、処理は"中断状態"のままで、リソースは開放されない

withUnsafeContinuationとwithCheckedContinuationの違い

  • withUnsafeContinuation
    • 使用方法のチェックを行わないため、パフォーマンスが良い
  • withCheckedContinuation
    • 使用方法のチェックを行ってくれる。
      • resumeを複数回呼び出すと、アプリケーションをとめてくれる
      • resumeを一度も呼び出さないと、ログを出力してくれる

実際に誤使用してみた

以下のようなコードを用意して挙動を確認する。

  1. continuation.resume(returning: str)の部分を複数回実行する。
  2. continuation.resume(returning: str)の部分をコメントアウトする。
import Foundation

//Completion Handlerを使った非同期処理
func method(completion: @escaping (String)->Void) {
    DispatchQueue.main.async {
        completion("Success")
    }
}

// withUnsafeContinuationを使用して、async/awaitに書き換える
func method_async_unsafe() async -> String {
    await withUnsafeContinuation{ continuation in
        method { str in
            continuation.resume(returning: str)
        }
    }
}

// withCheckedContinuationを使用して、async/awaitに書き換える
func method_async_checked() async -> String {
    await withCheckedContinuation { continuation in
        method { str in
            continuation.resume(returning: str)
        }
    }
}

// Async/Awaitでの呼び出し
/// withUnsafeContinuation
Task {
    let str = await method_async_unsafe()
    print("Status is \(str)")
}

/// withCheckedContinuation
Task {
    let str = await method_async_checked()
    print("Status is \(str)")
}

1..resume()を複数回呼び出す

  • withUnsafeContinuation
    • Playgroundだと何もおこらなかった。(出力なし、クラッシュなし)
    • Appのコードでやると、EXC_BAD_ACCESSでクラッシュした。
  • withCheckedContinuation
    • Fatal Errorが発生する
    _Concurrency/CheckedContinuation.swift:164: Fatal error: SWIFT TASK CONTINUATION MISUSE: method_async_checked() tried to resume its continuation more than once, returning Success!
    

2..resume()を呼び出さない

  • withUnsafeContinuation
    • 何もおこらない
  • withCheckedContinuation
    • 以下のwarningがログに出力される
    SWIFT TASK CONTINUATION MISUSE: method_async_checked() leaked its continuation!
    

References

Discussion