[Swift]WithCheckedContinuationが導入された背景
概要
Swift Evolutionを読んで withUnsafeContinuation
とwithCheckedContinuation
が導入された背景をまとめる。
withUnsafeContinuation
とwithCheckedContinuation
は、従来の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)
-
method1
はすぐに実行される -
return
のタイミングで "中断状態"になる - 非同期処理が終わったタイミングで
resume()
を呼ぶ。 -
継続
はシステムにスケジュールされたとき、実行される。(ここでは、継続はprint(result)
以降の処理)
規則
-
continuation.resume()
はちょうど1回呼ばれないといけない-
.resume
を複数回呼んだときの動作は、未定義となっている -
.resume
を一度も呼ばないと、処理は"中断状態"のままで、リソースは開放されない
-
withUnsafeContinuationとwithCheckedContinuationの違い
-
withUnsafeContinuation
- 使用方法のチェックを行わないため、パフォーマンスが良い
-
withCheckedContinuation
- 使用方法のチェックを行ってくれる。
-
resume
を複数回呼び出すと、アプリケーションをとめてくれる -
resume
を一度も呼び出さないと、ログを出力してくれる
-
- 使用方法のチェックを行ってくれる。
実際に誤使用してみた
以下のようなコードを用意して挙動を確認する。
-
continuation.resume(returning: str)
の部分を複数回実行する。 -
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)")
}
.resume()
を複数回呼び出す
1.-
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!
-
.resume()
を呼び出さない
2.-
withUnsafeContinuation
- 何もおこらない
-
withCheckedContinuation
- 以下のwarningがログに出力される
SWIFT TASK CONTINUATION MISUSE: method_async_checked() leaked its continuation!
Discussion