LoginSignup
3
6

More than 1 year has passed since last update.

非同期処理をasync/awaitを利用して書くことのメリット

Last updated at Posted at 2021-10-13

WWDC21で発表され、もはやどこでも発表されつくされた感のあるasync / awit。

遅ればせながら私も async/awaitをやっときちんと調べましたので、記事の形にしてみようと思います。

1. 非同期処理をclosure(もしくはdelegate)を利用して書く場合の注意点

例えば、下記のようなコードを書きたいとします。よくある非同期処理ですね。

func someLongLongTimeTask(completion: @escaping ((Data) -> Void), errorHandler: @escaping ((Error) -> Void)) {
    precheck()
    guard isReady else  {
        errorHandler(LongLongFuncError.setupFailure)
        return
    }

    someLongLongLong(completion: { data in
        guard self.validate(of: data) else {
            errorHandler(LongLongFuncError.dataValidationFailure)
            return
        }
        guard self.updated else {
            errorHandler(LongLongFuncError.selfUpdateFailure)
            return
        }

        completion(data)
    })
}

上記のコードでは、処理の完了やエラーの発生を引数として渡しているclosureで管理しようとしています。
ですが、closureは引数として渡されているため、Swiftのコンパイラ側で発火を保証することができません。
例えばこのコードは下記のように書き換えたとしても正常に動作します。

func someLongLongTimeTask(completion: @escaping ((Data) -> Void), errorHandler: @escaping ((Error) -> Void)) {
    precheck()
    guard isReady else  {
        return
    }

    someLongLongLong(completion: { data in
        guard self.validate(of: data) else {
            return
        }
        guard self.updated else {
            return
        }

        completion(data)
    })
}

上記コードはerrorHandlerの呼び出しをサボっただけのコードですが、この場合呼び出し元はエラーの発生を検知できません。その結果プログレスバーが表示されたままになるなどのバグが生まれる可能性があります。
またエラーの発生が極めて限定的な状況下で発生する場合、動作テストや社内テストで検知することができず、お客様トラブルへと発展する恐れがあります。

上記のテストコードはclosureを利用したコードでしたが、これをdelegateを利用して読み替えたとしても同様にdelegateの発火を強制することはできませんし、コンパイラに確認させることもできません。

このように、closure(もしくはdelegate)を利用した非同期処理はその性質上注意深く実装し、またテストを十二分に行わないと容易にバグを生む可能性がある コードになります
この問題を解決するのが async/awaitです。

2. 上記のコードをasync/awaitを利用して書き換えてみる

上記のコードをasync/awaitを利用して書き換えてみると下記のようになります。

func someLongLongTimeTask() async throws -> Data {
    precheck()
    guard isReady else { throw LongLongFuncError.setupFailure }

    let data = try await someLongLongLong()

    guard validate(of: data) else { throw LongLongFuncError.dataValidationFailure}
    guard updated else { throw LongLongFuncError.selfUpdateFailure}

    return data
}

上記のようにasync/awiatを利用してコードを書き換えると下記のようなメリットが生まれます
- closureを利用することによるコードのネスト構造はなくなり、直線的なコードになりました。
- また、guard節を利用したvalidationも明快になり、同期関数と似たような見た目になりました
- そしてさらに、Swiftのコンパイラがエラーのthrowかデータの返却を確認するようになりました。

例えば、下記のようなコードはコンパイルが通りません。これはつまり、SwiftコンパイラによってErrorのThrowを保証できているということになります。

func someLongLongTimeTask() async throws -> Data {
    precheck()
    guard isReady else { return }

    let data = try await someLongLongLong()

    guard validate(of: data) else { return }
    guard updated else { return }

    return data
}

このように、非同期処理にasync/awaitを利用することで、closureやdelegateを利用した非同期処理より、よりかんたんに、そしてより安全にコードを記述することができます。

3. async/awaitの注意点

async/awaitはこのように安全にコードを書くことができますが、いつ処理が発火されるかわからないため、状態管理やスレッドには十分に注意を払う必要がある という点には注意が必要です。
async/awaitを利用すると、それぞれの処理がいつ行われるかはOSによって決定されるため、実行されるThreadは多くの場合呼び出しスレッドとは違うでしょうし、もしかしたら更新中の配列に同時アクセスしてデータ競合や内部状態の破損をもたらしてしまうかもしれません。

このような問題の多くはasync/awaitと同時に発表されたactorという概念を利用することで解決できます
(後日こちらについても記事を書く予定です)

4. おわりに

このようにasync/awaitを利用して非同期処理を記述すると、closure /delegateを利用して書く場合に比べてよりかんたんに、そして安全にコードを書くことができます。
データの非同期処理をかんたんにするためにSingleを利用していたプロジェクトなどは、async/awaitに切り替えることで外部ライブラリの仕様を減らすことができ、良いのではないでしょうか。
(私が参画しているプロジェクトでも、十分に検討した上で随時async/awaitへの移行を進めようと考えています)

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6