Swiftの非同期処理「async/await」をわかりやすく解説!基本から応用まで

Swiftは、Appleの開発言語として広く使用されていますが、アプリケーションのパフォーマンスとユーザー体験を向上させるために、非同期処理は非常に重要な役割を果たします。非同期処理を利用することで、時間のかかるタスク(例:ネットワーク通信、ファイルの読み書き、データベースアクセスなど)をメインスレッドの外で実行し、UIの操作性を維持することが可能です。

従来、Swiftではコールバックやクロージャ、GCD(Grand Central Dispatch)などの手法を用いて非同期処理を行っていましたが、これらはコードが複雑になりやすく、保守性に課題がありました。そこで、Swift 5.5から「async/await」という新しい非同期処理の仕組みが導入され、コードがよりシンプルで読みやすくなりました。

本記事では、「async/await」の基本から応用まで、非同期処理に関する知識を深め、効率的なSwiftプログラミングを目指すための解説を行います。

目次

非同期処理とは何か

非同期処理とは、プログラムが他の作業を待たずに、同時進行で別のタスクを実行できる仕組みのことです。通常、プログラムは指示された処理が終わるまで次の処理に進めませんが、非同期処理では一部のタスクをバックグラウンドで実行し、メインの操作や処理が中断されることなく進行できます。

非同期処理の必要性

非同期処理は、ユーザー体験を向上させるために不可欠です。特に、次のような状況では非同期処理が重要です。

1. ネットワーク通信

APIの呼び出しやデータのダウンロードは時間がかかるため、メインスレッドが待機すると、アプリケーションが応答しなくなる可能性があります。

2. 入出力操作

ファイルの読み書きや、データベースへのアクセスも同様に、時間がかかる操作です。これらを非同期で実行することで、UIが凍結することを防げます。

非同期処理により、アプリケーションのパフォーマンスが向上し、ユーザーが快適に操作を続けられるようにすることができるため、モダンなアプリケーションには不可欠な技術です。

Swiftにおける非同期処理の進化

Swiftが登場してから、非同期処理は様々な手法を経て進化してきました。従来の非同期処理手法であるコールバックやクロージャ、そしてGCD(Grand Central Dispatch)は強力ですが、コードが複雑化しやすく、特に複数の非同期タスクを扱う際に「コールバック地獄」と呼ばれる問題が発生することがありました。

従来の非同期処理手法

1. コールバック

非同期処理の結果を関数の引数として渡す方法です。シンプルですが、処理がネストしていくと、コードが読みにくくなるという課題がありました。

fetchData { data in
    processData(data) { result in
        updateUI(result)
    }
}

2. クロージャ

Swiftで非同期処理に使用されることが多いクロージャは、ブロックとして定義されたコードを後で実行できる手法です。しかし、処理が複雑になると同様にネストが深くなり、保守性が低下します。

3. GCD(Grand Central Dispatch)

GCDは、バックグラウンドでタスクを非同期的に処理するための強力なAPIです。特定のキューでタスクを実行したり、複数のタスクを並列に実行することができますが、コードがやや冗長になりがちです。

DispatchQueue.global().async {
    let data = fetchData()
    DispatchQueue.main.async {
        updateUI(with: data)
    }
}

「async/await」の登場

これらの課題を解決するために、Swift 5.5から「async/await」が導入されました。この新しい非同期処理モデルにより、非同期タスクを直線的に書けるため、コードの可読性が飛躍的に向上します。「async/await」によって、非同期処理を同期的なコードと同じように書けるため、これまでの「コールバック地獄」やGCDの複雑な管理が不要になりました。

「async/await」とは何か

「async/await」は、非同期処理を同期的なコードのように記述できるようにする新しい構文です。これにより、複雑な非同期処理がシンプルで読みやすい形で書けるようになり、Swiftプログラムの保守性や可読性が向上します。

asyncとawaitの基本的な仕組み

asyncは非同期処理を行う関数の定義に使用され、awaitはその関数が非同期で実行されることを示すために使用されます。これにより、非同期タスクが完了するまで他の処理が一時停止しますが、コード自体は直線的に記述されます。

func fetchData() async -> String {
    // 非同期でデータを取得する
    return "データ取得完了"
}

func updateUI() async {
    let data = await fetchData()
    print(data)
}

上記の例では、fetchData()が非同期関数として定義され、updateUI()の中でawaitを使ってその実行が待たれています。awaitを使うことで、非同期処理が完了するまで他の処理をブロックしますが、見た目は同期処理と同じような構造になっています。

「async/await」を使うメリット

1. コードの可読性向上

従来の非同期処理ではコールバックやクロージャが多用され、処理が複雑化してしまうことがありました。しかし、「async/await」を使うことで、非同期処理を直線的に書けるため、コードの見通しが良くなります。

2. エラーハンドリングの一貫性

「async/await」は、従来の非同期処理手法に比べて、エラーハンドリングがより一貫して行えるというメリットがあります。do-catch文と組み合わせることで、同期処理と同じようにエラーを処理することが可能です。

func fetchData() async throws -> String {
    // エラーが発生する可能性のある非同期処理
    throw NSError(domain: "FetchError", code: 1, userInfo: nil)
}

do {
    let data = try await fetchData()
    print(data)
} catch {
    print("エラーが発生しました: \(error)")
}

このように、非同期処理が完了するまでコードの流れを同期的に制御しながら、エラーハンドリングも簡潔に行うことができます。

「async/await」は、非同期処理をより直感的に記述できるようにする、Swiftの強力な機能です。これにより、複雑な処理のシンプル化とメンテナンス性の向上が期待できます。

非同期関数の作り方

非同期関数は、通常の関数と似ていますが、非同期に実行されることを示すためにasyncキーワードを追加します。非同期関数は、時間がかかる処理をバックグラウンドで行い、呼び出し元の処理をブロックすることなく結果を返します。ここでは、基本的な非同期関数の作成方法について具体的なコード例を交えながら説明します。

非同期関数の定義方法

非同期関数を定義する際は、asyncキーワードを関数のシグネチャに追加する必要があります。以下の例では、簡単な非同期関数を定義しています。

func fetchData() async -> String {
    // 2秒間の待機をシミュレート
    await Task.sleep(2 * 1_000_000_000) // 2秒のスリープ(ナノ秒単位)
    return "データ取得完了"
}

この関数は、2秒間待機した後に「データ取得完了」という文字列を返します。Task.sleep関数を使用して非同期に2秒待機するようにしています。awaitキーワードを使って待機の完了を示しますが、この操作はメインスレッドをブロックしないので、アプリのUIが止まることはありません。

非同期関数の呼び出し

非同期関数を呼び出す際には、awaitキーワードを使って、その処理が完了するのを待ちます。非同期処理が完了するまで次の処理は一時停止されますが、コードの流れは直線的で読みやすい形になります。

func updateUI() async {
    let data = await fetchData()
    print(data) // "データ取得完了"が表示される
}

awaitキーワードを使うことで、fetchData()が非同期で実行され、処理が完了した後にdataの値が取得されます。

戻り値がない非同期関数

場合によっては、非同期関数が結果を返さないこともあります。こうした関数もasyncキーワードを用いて定義します。

func performTask() async {
    // 何か時間がかかる処理
    await Task.sleep(1 * 1_000_000_000) // 1秒のスリープ
    print("タスク完了")
}

このように、非同期処理の結果を返さない場合でも、async関数を使用して非同期タスクを実行することができます。

非同期関数を使う上での注意点

非同期関数は、基本的にメインスレッド上で直接呼び出すことはできません。非同期関数を実行するためには、非同期コンテキスト(async関数内など)で呼び出すか、Taskを使って非同期タスクを生成して呼び出す必要があります。

Task {
    await updateUI()
}

このコードは、メインスレッドで非同期処理を開始するための方法です。Taskを利用することで非同期関数を安全に呼び出すことができます。

非同期関数の作り方を理解することで、非同期処理をシンプルで直感的に実装できるようになります。「async/await」を活用することで、コードの可読性を維持しながら、効率的な非同期処理が可能です。

async/awaitの使い方

「async/await」は、非同期処理を直線的に記述できる強力なツールです。これにより、従来の複雑なコールバック処理やクロージャに代わり、簡潔で理解しやすいコードが書けるようになります。ここでは、「async/await」の基本的な使い方を、具体的なコード例を通して説明します。

基本的な使用例

まず、基本的な非同期処理の例を見ていきましょう。非同期でデータを取得し、それをUIに反映するという単純な例を考えます。

func fetchData() async -> String {
    // 非同期でデータ取得をシミュレート
    await Task.sleep(2 * 1_000_000_000) // 2秒待機
    return "データ取得完了"
}

func updateUI() async {
    let data = await fetchData()
    print("UIが更新されました: \(data)")
}

この例では、fetchData()という非同期関数がデータを取得し、その結果をupdateUI()関数で使っています。awaitキーワードを使用して、非同期処理の完了を待ちますが、コードの流れは直線的で、同期処理のように見やすくなっています。

複数の非同期関数を連続して実行する

複数の非同期関数を連続して実行する場合も、「async/await」は非常に便利です。非同期関数を順番に呼び出し、処理を一つずつ待機することができます。

func fetchUserProfile() async -> String {
    await Task.sleep(1 * 1_000_000_000)
    return "ユーザープロフィール"
}

func fetchUserPosts() async -> String {
    await Task.sleep(1 * 1_000_000_000)
    return "ユーザーの投稿"
}

func loadData() async {
    let profile = await fetchUserProfile()
    print("プロフィール: \(profile)")

    let posts = await fetchUserPosts()
    print("投稿: \(posts)")
}

この例では、ユーザープロフィールと投稿データを順番に非同期で取得しています。それぞれの処理が終わるのを待ってから次の処理を開始するため、コードの可読性が非常に高いです。

並列処理を使用した最適化

場合によっては、複数の非同期タスクを並列に実行したいことがあります。このような場合、「async let」を使うことで、並列に非同期処理を行うことができます。以下の例では、プロフィールと投稿データを同時に取得し、全体の処理時間を短縮しています。

func loadDataInParallel() async {
    async let profile = fetchUserProfile()
    async let posts = fetchUserPosts()

    let userProfile = await profile
    let userPosts = await posts

    print("プロフィール: \(userProfile)")
    print("投稿: \(userPosts)")
}

このコードでは、async letを使ってfetchUserProfile()fetchUserPosts()を並列に実行し、両方の処理が終わった後で結果を取得しています。これにより、処理全体のパフォーマンスが向上し、効率的な非同期処理が可能になります。

async/awaitの応用例:非同期API呼び出し

実際の開発では、ネットワーク通信などのAPI呼び出しを非同期で行うことが一般的です。以下の例は、非同期のAPIリクエストをasync/awaitを使って行う例です。

func fetchDataFromAPI() async throws -> Data {
    let url = URL(string: "https://example.com/api")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

func processAPIResponse() async {
    do {
        let data = try await fetchDataFromAPI()
        print("APIから取得したデータ: \(data)")
    } catch {
        print("APIリクエストに失敗しました: \(error)")
    }
}

この例では、URLSession.shared.data(from:)async/awaitでラップして非同期のAPI呼び出しを行っています。do-catchブロックを使用してエラーハンドリングも簡潔に行えます。

async/awaitを使う利点

  1. コードの可読性向上:非同期処理が同期処理のように直線的に書けるため、コードが簡潔になり、メンテナンスが容易になります。
  2. エラーハンドリングの一貫性do-catchを使った例外処理と組み合わせて、非同期処理でもエラーを効果的に管理できます。
  3. パフォーマンスの最適化async letを使って並列処理を行うことで、効率的な処理が可能です。

「async/await」を使うことで、非同期処理をよりシンプルで効率的に実装できるため、現代のSwift開発において重要な技術となっています。

例外処理との連携

非同期処理を行う際には、エラーハンドリングも重要な要素の一つです。特に、ネットワーク通信やファイルアクセスなど、外部リソースに依存する処理ではエラーが発生する可能性が高いため、適切に例外処理を行うことが求められます。「async/await」と組み合わせることで、例外処理を同期処理と同様の流れでシンプルに記述することができます。

非同期関数での例外処理

非同期関数内でエラーハンドリングを行うためには、throwsキーワードを使用します。非同期関数がエラーを発生させる可能性がある場合、関数宣言にthrowsを追加し、呼び出し側でtryawaitを組み合わせて処理を行います。

func fetchData() async throws -> String {
    let success = Bool.random() // 成功か失敗かをランダムに決定
    if success {
        return "データ取得成功"
    } else {
        throw NSError(domain: "FetchError", code: 1, userInfo: nil)
    }
}

この関数はランダムでエラーを発生させるシンプルな例です。throwsキーワードを使用してエラーをスローできるようにしています。

try/awaitの使い方

エラーハンドリングを行う際、非同期関数を呼び出す部分にtryを追加し、エラーが発生した場合にキャッチするためにdo-catchブロックを使用します。

func loadData() async {
    do {
        let data = try await fetchData()
        print("取得したデータ: \(data)")
    } catch {
        print("エラーが発生しました: \(error)")
    }
}

このコードでは、tryを使ってfetchData()を呼び出し、エラーが発生した場合はcatchブロックで例外処理を行っています。try/awaitを使うことで、非同期処理と例外処理が統合され、コードが非常にシンプルになります。

非同期タスク内でのエラーハンドリング

また、非同期タスク内でもエラーハンドリングが可能です。非同期タスクは通常Taskを使用して実行しますが、タスク内でtry/awaitを使うことで例外処理を実装できます。

Task {
    do {
        let data = try await fetchData()
        print("タスク内で取得したデータ: \(data)")
    } catch {
        print("タスク内でエラーが発生しました: \(error)")
    }
}

この例では、Task内で非同期処理を行い、エラーハンドリングもdo-catchで行っています。このように、非同期タスク内でもasync/awaitと例外処理が簡単に組み合わせられるため、エラーに対応した信頼性の高い非同期処理を実装できます。

例外処理のベストプラクティス

非同期処理における例外処理の実装にはいくつかのベストプラクティスがあります。

1. 適切なエラーの種類を使用する

Swiftでは、標準のErrorプロトコルを使用してカスタムエラーを定義できます。具体的なエラーの種類を使用することで、エラーメッセージをより分かりやすく、デバッグしやすくなります。

enum DataError: Error {
    case networkFailure
    case invalidResponse
}

2. 非同期処理におけるエラーの伝播

エラーが発生した場合、適切な箇所でキャッチしてエラーを処理するか、さらに上位の関数に伝播させることが重要です。例えば、非同期処理を行う関数が別の非同期関数を呼び出す場合、それぞれの関数でエラーハンドリングを行うか、全てのエラーを最終的に一箇所で処理することを検討します。

まとめ

「async/await」と例外処理を組み合わせることで、非同期処理中に発生するエラーに対する対応がシンプルになり、保守性が向上します。従来の非同期処理では複雑になりがちだったエラーハンドリングも、try/awaitを使用することで直線的に書けるため、コードの読みやすさも大きく改善されます。

並列処理と「async let」

「async/await」を使用すると、非同期処理を直線的に記述することができますが、複数の非同期タスクを同時に実行する必要がある場合、並列処理が非常に有効です。Swiftでは、並列処理を簡単に実現するために「async let」を使用できます。これにより、複数の非同期タスクを同時に実行し、効率的に処理を行うことが可能になります。

async letの基本

通常、非同期処理は順番に実行されますが、複数の非同期タスクを並列で処理したい場合は「async let」を使って同時に処理を開始することができます。各タスクの結果が必要になるタイミングでawaitを使って待機することで、効率的な非同期処理が実現します。

func fetchDataA() async -> String {
    await Task.sleep(2 * 1_000_000_000) // 2秒待機
    return "データA取得"
}

func fetchDataB() async -> String {
    await Task.sleep(3 * 1_000_000_000) // 3秒待機
    return "データB取得"
}

func loadAllData() async {
    async let dataA = fetchDataA()  // 並列で実行
    async let dataB = fetchDataB()  // 並列で実行

    let resultA = await dataA  // ここでデータAを待機
    let resultB = await dataB  // ここでデータBを待機

    print("データA: \(resultA)")
    print("データB: \(resultB)")
}

この例では、fetchDataA()fetchDataB()を並列に実行し、それぞれの結果が必要になった時点でawaitで待機しています。これにより、2つの非同期タスクが同時に進行するため、合計5秒かかる処理が実際には最大で3秒で完了します。

async letを使った並列処理のメリット

async letを使うことで、次のようなメリットがあります。

1. 処理速度の最適化

複数の非同期タスクを同時に実行することで、全体の処理時間が短縮されます。特に、各タスクが互いに依存せず、並行して実行できる場合に効果を発揮します。

2. コードの可読性向上

複数の非同期タスクを扱う場合、従来の方法ではGCDやクロージャを使って複雑なコードになりがちでしたが、async letを使うことで、コードが直線的に書けるため、可読性が大幅に向上します。

タスクのキャンセルに注意

async letを使って並列処理を行う際には、タスクのキャンセルにも注意が必要です。各非同期タスクは、必要がなくなった場合に適切にキャンセルすることで、リソースの無駄を避けることができます。

キャンセル処理の実装

Task.cancel()メソッドを使うことで、非同期タスクのキャンセルが可能です。また、タスクがキャンセルされると、CancellationErrorがスローされます。

func loadDataWithCancel() async {
    let task = Task {
        async let dataA = fetchDataA()
        async let dataB = fetchDataB()

        let resultA = await dataA
        let resultB = await dataB

        print("データA: \(resultA)")
        print("データB: \(resultB)")
    }

    // 必要に応じてキャンセル
    task.cancel()

    // タスクがキャンセルされたか確認
    if task.isCancelled {
        print("タスクがキャンセルされました")
    }
}

この例では、Taskオブジェクトを使用して非同期タスクをキャンセルしています。task.cancel()を呼び出すことで、並列で実行中のタスクがキャンセルされ、不要な処理が行われないようになります。

並列処理の適用例

並列処理は、特に以下のような場面で有効に機能します。

1. 複数のAPIリクエスト

同時に複数のAPIからデータを取得する場合、各リクエストを並列で実行することで、全体の待機時間を大幅に短縮できます。

2. データの並列処理

複数のファイルの読み込みや、計算処理を並列で実行することで、パフォーマンスが向上します。

func loadMultipleFiles() async {
    async let fileA = readFile("fileA.txt")
    async let fileB = readFile("fileB.txt")

    let resultA = await fileA
    let resultB = await fileB

    print("ファイルAの内容: \(resultA)")
    print("ファイルBの内容: \(resultB)")
}

このように、複数のファイルを同時に読み込むことで、待機時間を短縮し、処理の効率を高めることができます。

まとめ

「async let」を使うことで、非同期処理を並列で実行し、処理時間を最適化できます。複数のタスクを同時に実行することで、リソースの効率的な利用が可能となり、アプリケーションのパフォーマンスが向上します。並列処理は特に、APIリクエストやデータの処理、ファイル読み込みなど、複数のタスクが互いに依存しない場合に強力な効果を発揮します。

タスクのキャンセル処理

非同期処理を実行している際、ユーザーのアクションやシステムの状況によって、タスクを途中でキャンセルしたい場合があります。Swiftの「async/await」では、キャンセル可能なタスクを簡単に扱うことができ、タスクが不要になったときにキャンセルすることで、リソースを効率的に管理できます。

タスクのキャンセルの仕組み

Swiftの非同期タスクはキャンセル可能です。キャンセルは通常、ユーザーが処理を中止した場合や、複数の並列タスクのうち一部が不要になった場合に行われます。タスクをキャンセルすると、タスクの実行が中断され、残りの処理は実行されません。

キャンセル処理を実装するために、Task.cancel()メソッドが提供されています。また、キャンセルされたタスクは、CancellationErrorをスローして、その状態を知らせます。

キャンセル可能なタスクの作成

以下は、キャンセル可能なタスクの基本的な例です。この例では、タスクがキャンセルされた場合、キャンセルされたことを確認する処理を行っています。

func fetchData() async throws -> String {
    for i in 1...5 {
        try Task.checkCancellation() // キャンセルされたか確認
        await Task.sleep(1 * 1_000_000_000) // 1秒待機
        print("処理中: \(i)秒経過")
    }
    return "データ取得完了"
}

このコードでは、Task.checkCancellation()メソッドを使って、タスクがキャンセルされたかどうかをチェックしています。もしキャンセルされていれば、このメソッドはCancellationErrorをスローし、処理が中断されます。

タスクのキャンセル方法

タスクをキャンセルするには、Taskオブジェクトを作成し、その上でcancel()メソッドを呼び出します。以下はその例です。

func loadData() async {
    let task = Task {
        return try await fetchData()
    }

    // 3秒後にタスクをキャンセル
    await Task.sleep(3 * 1_000_000_000)
    task.cancel()

    do {
        let result = try await task.value
        print("結果: \(result)")
    } catch is CancellationError {
        print("タスクがキャンセルされました")
    } catch {
        print("エラー: \(error)")
    }
}

この例では、Taskを使ってfetchData()関数を実行し、3秒後にタスクをキャンセルしています。task.cancel()メソッドが呼ばれた時点で、タスクのキャンセルが試みられます。そして、CancellationErrorがスローされ、キャンセルされたことを確認できます。

キャンセル状態の確認と対処

非同期タスクがキャンセルされると、Swiftはタスクのキャンセルを伝え、適切に処理を中断します。キャンセルされたかどうかを確認するには、Task.isCancelledプロパティを使用することもできます。このプロパティは、タスクがキャンセルされているかどうかをBoolで返します。

func loadDataWithCheck() async {
    let task = Task {
        for i in 1...5 {
            if Task.isCancelled {
                print("タスクがキャンセルされました")
                return
            }
            await Task.sleep(1 * 1_000_000_000)
            print("\(i)秒経過")
        }
    }

    // 3秒後にキャンセル
    await Task.sleep(3 * 1_000_000_000)
    task.cancel()
}

この例では、Task.isCancelledプロパティを使ってタスクがキャンセルされたかどうかを確認し、キャンセルされている場合には処理を中断しています。

実際のキャンセル処理の応用

キャンセル処理は、次のような場面で特に役立ちます。

1. ユーザーインターフェイスとの連携

ユーザーが操作をキャンセルした場合(例えば、ダウンロードやデータ取得の途中で「キャンセル」ボタンが押された場合)、進行中の非同期処理を中断することで、ユーザー体験を向上させます。

func cancelButtonTapped() {
    task?.cancel()  // キャンセルボタンが押されたときにタスクをキャンセル
}

2. ネットワークリクエストのキャンセル

バックグラウンドで複数のネットワークリクエストを実行している場合、一部のリクエストが不要になったときや、ユーザーがリクエストを中止したいときに、リソースの無駄を防ぐためにキャンセルが役立ちます。

3. バックグラウンドタスクの管理

データベースアクセスやファイルの読み書きなど、時間のかかるバックグラウンド処理をキャンセルすることで、システムリソースを効率的に管理できます。

まとめ

タスクのキャンセル処理は、ユーザーやシステムの状況に応じて、不要な処理を途中で中断し、リソースの無駄を防ぐために非常に重要です。Swiftでは、Task.cancel()CancellationErrorを使って非同期処理のキャンセルをシンプルに実装できるため、効率的かつ柔軟に非同期タスクを管理できます。キャンセル処理を適切に実装することで、アプリケーションのパフォーマンスとユーザー体験が向上します。

非同期シーケンスとfor await

非同期処理では、複数の値を逐次的に非同期で取得する場合があります。例えば、APIから複数回にわたってデータを取得したり、ストリーミングデータを処理する際に、このようなケースが見られます。このような状況に対応するため、Swiftでは「for await」ループを使用して非同期シーケンスを効率よく処理することができます。

for awaitの基本

通常のforループは、同期的なシーケンス(配列など)を反復処理しますが、非同期処理の場合にはfor awaitを使って非同期シーケンスを処理します。これにより、非同期的に提供される複数の値を1つずつ取得し、処理することができます。

以下の例では、非同期で複数のデータを取得し、それを順番に処理しています。

func fetchDataStream() async -> AsyncStream<String> {
    AsyncStream { continuation in
        continuation.yield("データ1")
        continuation.yield("データ2")
        continuation.yield("データ3")
        continuation.finish()
    }
}

func processData() async {
    for await data in fetchDataStream() {
        print("取得したデータ: \(data)")
    }
}

この例では、fetchDataStream()が非同期シーケンスを返し、そのシーケンスをfor awaitループで順番に処理しています。AsyncStreamは、非同期的に値を生成するシーケンスを表し、各データが準備できた時点でyieldによって順次供給されます。

非同期シーケンスの利点

非同期シーケンスを使うことで、次のようなメリットがあります。

1. ストリーミングデータの処理

リアルタイムでデータが供給されるストリーミングのようなケースで、for awaitを使うと、データが到着した瞬間に処理を行うことができます。たとえば、動画のストリーミング、センサーデータのリアルタイム処理、Webソケットのメッセージなどで効果的です。

2. 複数の非同期タスクの結果を順次処理

APIから段階的にデータを取得する場合など、複数の非同期リクエストを順次処理でき、個々のリクエストが完了した時点で次の処理に進むことができます。例えば、ユーザーの投稿を複数ページに分けて取得する場合などがこれに当たります。

for awaitを使った応用例

実際の開発では、例えば、非同期APIのレスポンスをストリームとして扱い、逐次的に処理する場面が多くあります。以下は、APIからのデータを非同期で取得し、順次処理する例です。

func fetchPaginatedData(page: Int) async -> String {
    await Task.sleep(1 * 1_000_000_000) // 1秒待機
    return "ページ \(page) のデータ"
}

func fetchAllData() async -> AsyncStream<String> {
    AsyncStream { continuation in
        for page in 1...3 {
            let data = await fetchPaginatedData(page: page)
            continuation.yield(data)
        }
        continuation.finish()
    }
}

func processPaginatedData() async {
    for await data in fetchAllData() {
        print("取得したデータ: \(data)")
    }
}

この例では、ページネーションされたデータを非同期で取得し、AsyncStreamを使って順次yieldで供給しています。for awaitループを使用することで、各ページのデータを順番に処理でき、全てのデータが取得されるまで待機します。

非同期シーケンスのエラーハンドリング

非同期シーケンスの処理中にエラーが発生する場合、通常のtryawaitを組み合わせてエラーハンドリングを行うことが可能です。

func fetchDataWithError() async throws -> AsyncThrowingStream<String, Error> {
    AsyncThrowingStream { continuation in
        continuation.yield("データ1")
        continuation.yield("データ2")
        continuation.finish(throwing: NSError(domain: "DataError", code: 1, userInfo: nil))
    }
}

func processDataWithErrorHandling() async {
    do {
        for try await data in fetchDataWithError() {
            print("取得したデータ: \(data)")
        }
    } catch {
        print("エラーが発生しました: \(error)")
    }
}

この例では、非同期シーケンス中にエラーが発生した場合、AsyncThrowingStreamを使ってエラーをスローし、for awaitループ内でtryを使ってエラーハンドリングを行っています。エラーが発生した時点で、ループは中断され、キャッチブロック内で適切な処理が行われます。

まとめ

非同期シーケンスとfor awaitは、非同期的に提供される複数の値を効率的に処理するための強力なツールです。ストリーミングデータやページネーションされたデータの取得、さらにはエラーハンドリングも簡単に行えるため、リアルタイム処理やデータの逐次処理が求められるアプリケーション開発において非常に有用です。これを活用することで、非同期処理の効率がさらに向上し、よりスムーズなプログラムが実現できます。

まとめ

本記事では、Swiftにおける非同期処理の基本から「async/await」の使い方、並列処理やタスクのキャンセル、非同期シーケンスの活用までを詳しく解説しました。「async/await」によって、非同期処理が同期的なコードのように簡潔に書けるようになり、コードの可読性や保守性が大幅に向上しました。

また、並列処理を行うためのasync letや、非同期タスクのキャンセル方法、非同期シーケンスを使った複数の値の逐次処理、エラーハンドリングの方法なども紹介しました。これらの機能を活用することで、複雑な非同期処理を効率的かつ直感的に実装できるようになります。

Swiftの「async/await」を習得することで、非同期処理がさらに効率化され、モダンなアプリケーションの開発において重要なスキルとなるでしょう。

コメント

コメントする

目次