[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)")
}
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!
Discussion