😬

[Swift]Async/Awaitが導入された背景

2023/08/20に公開

概要

Swift Evolutionを読んで async/await記法が導入された背景をまとめる。
Async/Await記法は従来のCompletion Handlerによる記法と比較して、以下の点で優れている。

  • ネストせずに複雑な処理を記述できる
  • コンパイラやツールのサポートを受けることができる
  • パフォーマンスが良い

従来のCompletion Handlerの問題点

従来の記法では、非同期処理を行うときには、Completion Handlerを用いるのが一般的であった。

// `Completion Handler`の例 
func asynFunc(completionBlock: (()->Void) {
    asyncOperation {
        // 非同期処理が終ってから、`Completion Block`を呼ぶ
        completionBlock()
    }
}

しかし、以下のような問題点があった。

  • ネストが深くなり、可読性が良くない
  • Completion Handlerの呼び忘れを防ぐのが難しい

ネストが深くなり、可読性が良くない

たとえば以下のようにネストが深くなると、デバッグや変更のときに、処理を追うのが難しい。

// ネストが深いときの例
func processImageData1(completionBlock: (_ result: Image) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource in // <= nest1
        loadWebResource("imagedata.dat") { imageResource in // <= nest2
            decodeImage(dataResource, imageResource) { imageTmp in // <= nest3
                dewarpAndCleanupImage(imageTmp) { imageResult in // <= nest4
                    completionBlock(imageResult)
                }
            }
        }
    }
}

Completion Handlerの呼び忘れを防ぐのが難しい

どのケースでも必ずCompletion Handlerを呼ぶことを期待していても、ネストが深い場合や条件分岐が複雑な場合に呼び忘れてしまうことがある。
また、Completion Handlerの呼び忘れをCompilerはチェックしてくれない。

// `Completion Handler`を呼び忘れてもエラーにはならない。 
func processImageData1(completionBlock: (_ result: Image?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource in // <= nest1
        guard hoge else {
            return // <- completionBlock を呼ばずにreturn
        }
        loadWebResource("imagedata.dat") { imageResource in // <= nest2
            guard hoge else {
                return
                completionBlock(nil)
            }
            decodeImage(dataResource, imageResource) { imageTmp in // <= nest3
                guard hoge else {
                    return // <- completionBlock を呼ばずにreturn
                }
                dewarpAndCleanupImage(imageTmp) { imageResult in // <= nest4
                    guard hoge else {
                        completionBlock(nil)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

Async/Awaitの利点

利点として以下の3つを挙げることができる

  • ネストせずに複雑な処理を記述できる
  • コンパイラやツールのサポートを受けることができる
  • パフォーマンスが良い

ネストせずに複雑な処理を記述できる

"従来の記法"のサンプルも、async/awaitでは、ネストなしで記述できる。
これによって複雑な処理も、可読性を損わずに記述できる。

// `Completion Handler`の例を`async/await`で書き変える
func processImageData1() async -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

コンパイラやツールのサポートを受けることができる

  • Completion Blockを使用せずに記述できるため、そもそも呼び忘れが発生しない。
  • 関数の戻り値はCompilerがチェックしてくれる。(想定された型をreturnし忘れるとerrorとなる)

パフォーマンスが良い

非同期処理では、途中で処理を中断し、別の処理が行われる。
処理間の移動のときに、処理の文脈(Context)を変える必要がある(Context Switching)。
Async/Awaitでは、処理を中断するときに、文脈の情報を、"継続(Continuation)オブジェクト"という形で、保存する。
また、処理を再開するときは、"継続オブジェクト"を読み込むことで、中断前の状態を復元できる。
処理の中断のときに、"Thread"の変更は行わないため、Context Switchingのコストが小さい。

References

Async/Awaitの利点

Async/Awaitのパフォーマンスについて

Discussion