🎃

【翻訳】Modern Concurrency in Swift: Introduction

2023/07/14に公開

はじめに

この記事シリーズは、もともと Xcode 13 ベータ 1 を使用して例を作成するために書かれました。
このシリーズの記事、コードサンプル、および提供されたサンプルプロジェクトは、
Xcode 13 ベータ 3 用に更新されました。

これは、Apple が WWDC2021 で発表した新しい async/await API に焦点を当てた
チュートリアルシリーズです。記事の数はまだ分かりませんが、今後数週間で掲載される予定です。

WWDC2021のセッションビデオは、これらの新しいAPIについて素晴らしい説明をしていますが、
新参者にとっても長年の開発者にとっても、まだ圧倒されることがあると感じています。
このシリーズで私が意図しているのは、新しい並行処理APIについて一歩ずつ説明し、
各記事でいくつかの概念を取り上げ、これらのAPIについて自信を持って理解できるようにすることです。
必要であれば、Appleが提供するスニペットを使ったり、修正したりすることもあります。
その場合は、この外部コードをそのように明示します。

このシリーズを通してあなたが目にする知識は、WWDC2021のセッション
(各記事に関連するセッションがリンクされています)や、私自身、そして他の情報源から得た知識です。
私は新しいawait/async APIのすべてを知っていると主張しているわけではありませんし、
Evolutionの提案はWWDC2021より前に承認されましたが、
WWDC2021より前にこの提案を調査したわけではないので、
私が持っている知識でこのシリーズを構成しています。

そのため、もし不正確な点を見つけたら、私が修正できるようご指摘ください。
このシリーズができる限り明確で正確であることは、私にとって非常に重要です。
誤字脱字に気を配り、おかしなところを見つけたらメールかツイッターで報告してほしい。

新しいAPIを探求する前に、現在のcurrent concurrencyとその問題点についてお話ししましょう。
この入門記事が終わるころには、新しいAPIがあなたの時間を投資する価値があることを
確信していることでしょう。

Concurrencyと現在の実装の問題点

WWDC2021でアップルは、開発者がアプリにConcurrencyを実装するための新しい方法を発表しました。
この2つの単語がすべての核心であるため、「async/await API」と呼ぶことにします。

開発者として、私たちはConcurrencyを知らないうちによく使ってきました。
iOS SDKでクロージャを取るほとんどすべての呼び出しは、非同期呼び出しであるため、
このようなシグネチャを持っています。iOS開発者であれば、
UIコードはいわゆるメインスレッドで実行されることを知っているでしょう。
UI操作はここで行われるため、何かが終了するのに非常に長い時間がかかると、
システムはアプリがフリーズしたと判断し、アプリを終了させるかもしれない。
一般的なソフトウェアにおけるconcurrencyの必要性は、多くの場合、何かの仕事を終わらせたり、
何かをスピードアップさせたりするために、並行タスクを生成することである。
アップルのテクノロジーに関して言えば、concurrencyの必要性は同じだが、
メインスレッドが何かにブロックされないように注意する必要もある。

メインスレッドをフリーズさせる可能性のある呼び出しは、アップルのSDKのいたるところにある。
このためアップルは、異なるスレッドに作業を委譲し、メイン・スレッドを解放しておくための
さまざまなツールを提供してくれている。

先に進む前に、これらの新しいAPIは標準になることが保証されているが、concurrencyに使われるのは
これだけではないことを覚えておいてほしい。async/awaitAPIがあなたのニーズをカバーできないと
感じた場合の代替手段については、詳しい記事がある。

コンシューマー向けAPIのコールバック・ベースの並行性

URLSession APIを例にとってみよう。WWDC2021以前は、ある種のネットワーキング・コールが必要な場合、
次のような呼び出しをしていた:

// ... (1)

let task = URLSession.shared.dataTask(with: ...) { data, response, error in
    // ... (2)
}

task.resume()

// ... (3)

コールバック・クロージャの中にあるもの、つまり中括弧{}の中にあるものはすべて、ダウンロードが行われた後に非同期で呼び出されるコードであり、どのような順番で呼び出されるかは保証されていない。
(1)の後に呼び出されることはわかっていますが、それだけです。

上のスニペットでは、ネットワーク呼び出し(1)の前に実行されるコードがあります。
しかし、(2)はすぐには実行されません。代わりに(3)の実行が続き、ダウンロードが終わると
(2)が実行されます。(2)と(3)の実行順序は保証されていません。
この特殊な例では、「明らかに」ネットワークコールはプログラムの線形実行よりも遅いと言えますが、
これを当然だと思わないでください。

これは機能するし、この古いスタイルのAPIはどこにも行かない。
しかし、後でJSONをパースしたり、レスポンスに基づいてさらにネットワークを呼び出したりする必要がある場合はどうでしょう?これは苦痛となり、私たちが「死のピラミッド」と呼ぶものに行き着きます。

let task = URLSession.shared.dataTask(with: ...) { data, response, error in
    let taskThatNeedsPreviousResponse = URLSession.shared.dataTask(with: ...) { data, response, error in
        let evenMoreNestedNetworking = URLSession.shared.dataTask(with: ...) { data, response, error in
            /// We can finally do more work here
        }
        evenMoreNestedNetworking.resume()
    }
    taskThatNeedsPreviousResponse.resume()

}

task.resume()

呼び出しがどんどんネスト(入れ子)になっていくと、可読性の面で問題になることがあります。
すべての "ピラミッド・フロア "を独自の関数に移動させることもできますが、
それはスコープを汚してしまうことになり、本当の解決策というよりは、パッチに過ぎません。

API設計者のためのコールバックベースのconcurrency

ここで、画像をダウンロードしてリサイズし、サムネイルを作成する関数を作成することになったとします。
このように書くことになるかもしれません。

func fetchThumbnail(for id: String, completion: @escaping (UIImage?, Error?) -> Void) {
    let request = thumbnailURLRequest(for: id)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(nil, FetchError.badID)
        } else {
            guard let image = UIImage(data: data!) else {
                return // (1)
            }
            image.prepareThumbnail(of: CGSize(width: 40, height: 40)) { thumbnail in
                guard let thumbnail = thumbnail else {
                    return // (2)
                }
                completion(thumbnail, nil)
            }
        }
    }
    task.resume()
}

(このコードは、AppleのMeet async/await in Swiftセッションからそのまま引用したものです。)

まず最初に気づくことは、このコードは口語的ということです。
ただ座って、それがどれほど長いかを理解してください。これは、画像をダウンロードすることから始まり、
次にそれをリサイズしようとします。ネットワーク呼び出しとサムネイルのサイズ変更呼び出しは、
どちらも非同期呼び出しです。それだけでなく、独自の完了ハンドラも渡す必要があります。

このコードにもバグがあり、見つけるのは難しいかもしれません。コールバックベースの関数を書いた場合、
何が起ころうとも渡されたコールバックを呼び出す必要があることを覚えておいてほしい。
あなたの関数を呼び出す開発者は、自分のコールバックが呼び出されないケースを見つけることができます。
上の例では、これが起こりうる場所が2箇所あります。それらを(1)と(2)としました。これらの場所では
完了ハンドラを呼び出していないので、API呼び出し元は決して到着しないレスポンスを待つことになります。
しかし、少なくともスレッドをブロックすることはないだろう。

ここで見つかる最初の問題は、
自分の仕事が終わったときにコールバックを呼び出す責任があるということだ。
これは小さな関数ではそれほど悪いことではありませんが、
考えなければならないエッジケースがたくさんあることに気づくと、圧倒されてしまいます。

しかし、コールバック・ベースのAPIで私がいつも嫌だと思うことのひとつは、
「リターン」タイプとエラーに関するすべての情報が、与えられたクロージャの一部になってしまうことです。
このため、返り値の型やエラーを返すかどうかを明記したクリーンなAPIを持つことができません。
エラーを投げることなどできないのです。これらのAPIでは。コールバックでエラーを提供しなければなりません。
静的型付けはなくなりませんが、より抽象化される(オートコンプリートは言うまでもなく、
このような呼び出しをするときに機能しないことがあるので、それほど便利ではないです。)
さらに、APIコンシューマーはエラーを破棄することもできます。

Combine?

Combineフレームワークはパイプラインを使うことで、上記の問題の多くを見事に解決していますが、
このシリーズではアップルのリアクティブ・フレームワークについてはあまり触れません。

第一に、Combineの将来が不透明だと感じているからです。
私自身はこのフレームワークが大好きで、
WWDC2021の当初は、これらの新しいAPIがその代わりを果たせるかどうか懐疑的だったのですが、
このトピックに関するセッションをいくつか見てから意見が変わりました。

第二に、十分に使われていないと感じているからです。
Combineは2019年に導入され、SwiftUIの存在を後押しする大きな役割を持っています(持っていた?)。
しかし、それが野放しになっている数年間、あまり採用されていないような気がします。
人々がコールバックベースのコードを置き換えるためにそれを採用している証拠はなく、
コミュニティリソースの欠如(いくつかは存在し、素晴らしい)とフレームワークのアップデートの欠如の両方は、
それに対するAppleの計画が何であるかを知るまで、
それに多くの時間を投資することは賢明ではないかもしれないと思わされます。

Combineについては、このチュートリアルシリーズを通して、関連性がない限りあまり触れません。
一般的に、コールバック・ベースのコードに取って代わる候補とはもはや考えていません。
私は非同期コードをFuturesでラップする大ファンでしたが。

新しい考え方

最後に、以下の記事に飛び込む前に、
concurrencyに関する現在の知識を窓から投げ捨ててみることをお勧めします。
というのも、async/awaitの実装は非常に異なっており、
async/awaitの仕組みを本当に理解する前に、この考え方を理解することが重要だからです。
一度async/awaitを理解すれば、ツールセットの残りの部分は理解しやすくなります。

現在持っているconcurrencyの知識が無意味になるとは言いません。
それどころか、concurrencyのコードを書きやすくすることで、過去数十年間Appleのプラットフォームで
concurrencyについてどのように考えてきたかを考え直す必要があるのは興味深いことです。
実際、async/awaitは手続き型プログラミングに似ているので、
非同期コードを見たことがない人には理解しやすいと思います。

これ以上言うまでもないが...。

以下の目次は、このシリーズの記事の一覧です。それぞれ独立した内容になっているので、
最後の記事だけが必要であれば、初期の記事を読む必要はありません。
とはいえ、async/awaitの初心者であれば、順番に読むべきでしょう。

記事の多くには、実行可能なコードが含まれています。
それをコピー&ペーストしたり、サンプル・プロジェクトがあればダウンロードしたりして、学習に役立ててください。

【翻訳元の記事】

Modern Concurrency in Swift: Introduction
https://www.andyibanez.com/posts/modern-concurrency-in-swift-introduction/

Discussion