SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

iOSエンジニアたちと振り返るWWDC

【WWDC2022直前!】WWDC2021で発表されたSwift Concurrencyを振り返る


階層構造を持った非同期処理を可能にするTaskとは

 Taskの階層構造と種類について紹介します。Taskとは非同期処理のタスクであり、非同期処理として実行できる処理の単位を表しています。このTaskは自身の子となるTaskを紐付けることによって、階層構造を持つことができます。

Taskの階層構造

 Taskは下記画像中にあるような階層構造を持つことができます。画像中の四角で囲まれているものがTaskを表しており、Taskを繋ぐ白線がTask間の親子関係を表しています。Taskがこのような構造を持つことによって、便利な規則を作成できる利点が生まれます。具体的には全ての子タスクが完了している際にその親のTaskは必ず完了する、というルールです。このルールは異常終了時にも適応されるため、エラーが発生した時にも必ずTaskは完了するようになっています。つまりTaskが必ず完了するように設計しておくことで、OSが依存関係にある各Taskの完了を保証することができ、未完了Taskが永久にメモリに残り続ける心配がなくなります。

 また、2つのTaskが並行処理中に一方のTaskがエラーとなった場合、何が起きるでしょうか。例えば、一方のmetadataと書かれたTaskが異常終了した場合は、もう一方のdataと書かれたTaskがキャンセルとマークされ処理は続行します。さらに、キャンセルとマークされたTaskの子孫にあたるTaskも全てキャンセルとマークされます。そして、全ての処理が完了するとエラーとしてTaskは完了します。このように、直ちに処理を終了しないのはエラーハンドルを開発者に委ねているからです。例えば、トランザクションが進行しているタスクがエラーとなった場合は、適切にトランザクションを終了する処理を記述できます。

 なお、現在実行中のタスクがキャンセルされているかは下記のようにTask.isCancelledを用いて調べることができます。

func task() async -> String {
    var result: String = ""
    if Task.isCancelled { ... }
    return result
}

Taskの構造化方法

 Taskは階層構造を持つように構造化できると先述しました。構造化の方法には2種類あり、それぞれに特徴があります。ここでは、async-let tasksとTask Groupの2つの方法を紹介します。

async-let tasks

 async-let tasksは最も簡潔に記述できる構造化されたタスクです。このタスクはコンパイル時に有限個と分かっているタスク群にしか適応できませんが、後述するTask Groupより簡単に記述できます。

 async-let tasksを利用するためには、下記のようにletやvarではなくasync letを使って変数を宣言し非同期関数の結果を保存します。この宣言によって現在のタスクに子タスクが生成されます。なお、この宣言をasync-let bindingsと呼びます。

async let result = URLSession.shared.data(...)
try await result

 async-let bindingsが実行された際に何が発生するかを下図を基に説明します。まず、letやvarを使った変数への代入と異なり、式の右辺が評価され、左辺に代入される前に、仮の値placeholderが代入されます。右辺の評価は、async-let bindingsによって生成された子タスクが並行して引き受けます。ここで、右辺の評価を子タスクが引き受けたことによって、async-let bindingsの後続処理を並行して進められます。最後に、async-let bindingsを使っていた変数をtry awaitを使って参照できます。この時には変数の中身はplaceholderではなく実際の値になっている必要があるため、この、async-let bindingsの右辺の評価を行う子タスクの終了を待ちます。

Task Group

 Task Groupは動的な数の子タスクを持つTaskを作ることができます。下図のfetchArticles(ids:)は、引数に受け取った複数のIDそれぞれに対応するArticleのDataを非同期で返します。関数内部では外部のエンドポイントを叩く関数fetchArticle(id:)をさらに呼び出し、非同期でArticleを取得しています。現在の実装ではTaskの構造化が行われていていないため、先述してきたようなTaskの構造化による利点を享受できていません。しかしこのようなロジックの場合、非同期処理の実行回数がfetchArticles(ids:)の引数に渡される配列の数というランタイム時に決定される値に依存するため、ランタイム前に記述するasync-let bindingsは使えません。

func fetchArticle(id: Int) async throws -> Data {
    // https://codezine.jp/article/detail/15285
    let request = URLRequest(url: URL(string: "https://codezine.jp/article/detail/\(id)")!)
    let (data, response) = try await URLSession.shared.data(for: request)
    if (response as? HTTPURLResponse)?.statusCode != 200 {
        throw NetworkError.failed
    }
    return data
}

func fetchArticles(ids: [Int]) async throws -> [Int: Data] {
    var articles: [Int: Data] = [:]
    for id in ids {
        articles[id] = try await fetchArticle(id: id)
    }
    return articles
}

 このような場合はwithThrowingTaskGroup(of:returning:body:)というAPIを使ってTask Groupを作ります。Task Groupを使って書き換えたのが下記のコードです。addTask(priority:operation:)の呼び出しによって、子タスクを生成することができます。そして、子タスクたちの結果はfor try await (id, article) in groupの記述によって、順に取り出すことができます。なお、このfor文を使って非同期処理の結果を受け取る方法の詳細はMeet AsyncSequenceにて紹介されています。

func fetchArticles(ids: [Int]) async throws -> [Int: Data] {
    var articles: [Int: Data] = [:]
    try await withThrowingTaskGroup(of: ((Int, Data).self)) { group in
        for id in ids {
            group.addTask {
                return (id, try await fetchArticle(id: id))
            }
        }
        
        for try await (id, article) in group {
            articles[id] = article
        }
    }
    return articles
}

次のページ
4種類目の型、Actorによる並行処理で競合状態を解決

この記事は参考になりましたか?

iOSエンジニアたちと振り返るWWDC連載記事一覧

もっと読む

この記事の著者

k-kohey(STORES 株式会社)(k-kohey)

 STORES 株式会社テクノロジー部門モバイル本部にて、STORES 決済 のiOSアプリ開発に従事。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/15663 2022/06/10 21:29

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー