ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftの並行処理: サンプルAppの更新
Swiftの並行処理を実演いたします。既存のサンプルAppを更新する様子をご覧ください。async/await、アクター、コンティニュエーションを実際に扱ってみましょう。既存のコードをSwift並行処理に徐々に移行するためのテクニックも検証します。 このセッションを最大限活かしていただくためには、事前にWWDC21の「Swiftのasync/awaitについて」と「Swiftアクターによるミュータブルステートの保護」をご確認いただくことをお勧めします。
リソース
関連ビデオ
WWDC22
WWDC21
WWDC20
-
ダウンロード
♪ (Swiftの並行処理: サンプルAppの更新) こんにちはSwiftチームのBenです このビデオでは Swiftの 新しいコンカレンシー機能を使用するために 既存のAppをポーティングする方法を説明します ここでは これらの新機能が より明確な非同期コードの記述や 起こりうるレースコンディションからの保護に どのように役立つか またコードをこの新しい操作方法に 徐々に移行させるための 幾つかのテクニックを見ていきます 今回使用するのは「Coffee Tracker」というAppで これはWWDC 2020で発表された「ウォッチ コンプリケーションの作成と更新」 というテーマに基づいています これは あなたが今日飲んだすべてのコーヒーを 記録することができるシンプルなAppで 現在のカフェインレベルを ウォッチフェイスに表示する 複雑な機能も備えています 小さなAppではありますが SwiftUIでのコンカレンシー watch SDKからのデリゲートコールバック AppleのSDKでの非同期APIとの連携など 私たちが考えたい様々なことを示しているので 目的に応じて使用するには 良い例です それでは このAppを簡単にご紹介しましょう Appは大きく分けて 3つのレイヤーに分かれています まず UIレイヤーがあります これは主にSwiftUIのビューですが 複雑なデータソースのようなものも UIレイヤーの一部と考えることができます 次にモデル層があり カフェイン飲料を表すいくつかの シンプルな値の型と 「コーヒーモデル」と呼ばれる モデル型で構成されています さて これはUIモデルと 呼ばれると思われるものです つまりUIレイヤーで表示するための データを保持する場所です これは SwiftUIのビューを 供給する観測可能なオブジェクトで 全ての更新はメインスレッドで 行われる必要があります 私はこれをUIモデルと呼んでいますが それは Appの全てのデータの 完全なモデルではない可能性があるためです UIモデルは データモデルまたは そのサブセットに過ぎない 可能性があります つまり 現時点でUIに表示する 必要があるものなのです 最後に バックエンド層と呼ばれるものがあります これは恐らくバックグラウンドで行われる処理で モデルを投入したり Appの 外の世界と対話したりします 今回の例では HealthKit Controller というタイプがそれにあたります このタイプは ユーザーの カフェイン摂取量を保存したり 読み込んだりするための HealthKitとの通信を管理します さて コードを見始める前に App内での同時実行性の 管理方法について説明します このアーキテクチャは非常に すっきりしているように見えますが コンカレンシーの処理方法を重ねると より複雑な図式になります このAppは基本的にコードが実行される 3つの並列キューに分かれています UIとモデルでの作業はメインキューで行われます またAppにはバックグラウンドで 作業を行うための ディスパッチキューがあります そして最後に 完了ハンドラへの 特定のコールバック (HealthKitから結果を返すものなど)は 任意のキューで行われます これはかなり一般的な状況です 一見 シンプルなAppアーキテクチャに見えても 並列処理の方法には多くの複雑さが 隠れています さて ちょっとしたネタバレです Swiftのコンカレンシーを採用すると このような臨時的な並列処理アーキテクチャから 次のようなものになります UIビューとモデルを メインアクターと呼ばれるものに 置くことにします バックグラウンドで動作する 新しいアクターを作成し これらのタイプは async/await機能を使用し スレッドセーフな値を相互に渡します これが終わると並列処理アーキテクチャは 型のアーキテクチャと同様に 明確で簡単に説明できるようになります さて「async/await」や 「actor」など 聞き慣れない用語をいくつか使用しましたが コードで使用する際に簡単に説明します しかし これらの機能をより詳しく 説明するためには 他にもいくつかの講演が用意されているので そちらを参照してください さて 全体のアーキテクチャを見たところで 次にコードを見てみましょう ここに いくつかの異なるタイプがあります ウォッチフェイスに情報を表示するための 拡張デリゲートと同様に SwiftUIのビューレイヤーの一部があります そして 飲み物のモデリング用の シンプルなモデルタイプがあります そして UIモデルである CoffeeDataは飲み物の 配列を集約してSwiftUIに パブリッシュします 最後に HealthKitControllerです このレイヤーでは Swiftの 新しい並列処理機能のいくつかを Appに導入しようとしています HealthKitControllerには 完了ハンドラを取る HealthKit SDKへの 異なる呼び出しが幾つか含まれます まず このコントローラの 保存操作を見てみましょう Control-6を押すと このファイルに含まれる 関数のリストが表示されるので 保存操作を見てみましょう 新しい並列処理機能に入る前に 今日はSwiftにおけるスレッドの 安全性について説明します このコードはisAvailableや storeなどの変数にアクセスしています 私たちはこれらの変数をこの関数の中でしか 読んでいないように見えます これは安全でしょうか? いいえ 他のコードが同時に これらの変数に書き込んでいる 可能性がある場合は安全ではありません このコードがスレッドセーフか どうかを知るためには この関数を見るだけではなく もっと多くの情報が必要です ディスパッチキューやロックは使われていないので このコードがスレッドセーフである-- と仮定すると--どこかにあるはずです 発信者はキューを介して保存する ためにすべての呼び出しを シリアライズしているのかも しれません あるいは Appが 何等かの方法で構築されていて 問題なしという意味かもしれません しかし この関数を見ただけでは それは分かりません このように プログラムの他の部分を見に行くことなく この関数を見て特定のことを 知ることができることを 私たちはローカル推論と呼びますが これはSwiftにとって大変重要な目標です 例えば Swiftが値型を重視しているのは ローカル推論によるものです 参照型とは異なり 渡された値型が プログラムの他の場所で変更されていることを 心配する必要はありません Swift 5.5で並列処理のために 導入された言語機能の多くは コードをローカルに推論する機会を 増やすためのものです さて たまたまですが この関数は完全にスレッドセーフです 私は自分でそれを理解しました コンパイラはこれらの問題を 見つけるのを助けてくれません 従ってHealthKit SDKを呼び出して caffeineSampleを ユーザーの健康データに保存します これには 完了ハンドラが必要で その 完了ハンドラは2つの値を取ります successまたはerrorです 操作が成功した場合エラーはありません エラーはnilになります つまり ステータスのチェックを 忘れずに行い 必要に応じて ここでオプションのエラーを 解除する必要があります これは通常 Swiftでエラーを 処理する方法ではありません 失敗した時にスローする方法としては これの方がはるかに良いでしょう でも この方法は完了ハンドラでは動作しません しかし 非同期メソッドを使えば スローすることができる 非同期関数を持つことができます このHealthKitのsaveメソッドには それに相当する非同期メソッドがあるので それを使うように切り替えましょう そのためには 完了ハンドラを 削除して 新しい非同期メソッドの 前に「await」と書きます このawaitによって 非同期関数であることがわかり この時点でコードが一時停止して 他のコードが実行される可能性があります このことがなぜ重要なのかは後ほど説明します さて これで 先程お話したように コンパイラはこのコードがスローできるように なったことを教えてくれました これは 非同期関数の大きな利点で スローすることができます オプションのエラーをチェックすることを 覚えておく必要はもうありません この問題を解決するためにtryを追加して ここでエラーを処理したいと思います そして これをdo-catch ブロックで囲みます
これでガードも強制アンラップも不要になりました コードを再配置して saveの呼び出しのすぐ下に 成功したログを記録することで ハッピーパスを全て一緒に維持することができます saveが値を返さなくなった ことに注目してください 成功か失敗かを返すのはエラーと重複していたので 新しい関数では スローするか成功するかの どちらかだけにしました
try-catchを追加したところで コンパイラからもう1つエラーが出ています 非同期関数を呼び出していますが 同期関数の中から呼び出しています これではうまくいきません 非同期関数には 同期関数にはない機能があります それは 待機中に実行しているスレッドの制御を 放棄する機能です これを実現するために 非同期関数はスタックフレームを 別の方法で処理しますが これは同期関数とは互換性がありません そこで この同期関数を非同期関数にする という方法があります では やってみましょう これで このファイルはコンパイルされました しかし プロジェクト全体はまだ コンパイルされません この関数を非同期にしたことで 問題が一段と大きくなりました 私のデータモデルでは 同じコンパイラエラーが発生しています この関数は非同期ではないからです さて このまま連鎖していくことも可能ですが 今回は変更を局所化するための 別のテクニックを見てみましょう 非同期関数を呼び出すために 新しい非同期タスクを作成します
このタスクは非同期関数を呼び出すことができます
この非同期タスクは グローバルディスパッチキューで 非同期を呼び出すのと非常によく似ています ブロックは同時に実行されるので そこから外側の関数に値を返すことはできません そのため 取り外された クロージャー内で行うことは 自己完結型である必要があります この例ではsaveを呼び出していますが これは値を返さないので問題ありません また 他のスレッドから同時に 変更される可能性のある グローバルな状態に触れていないか 注意する必要があります そうでなければ この非同期タスクを追加することで 誤って新しい競合状態を 引き起こしてしまうかもしれません 非同期タスクの中に入れたことで 待ち受けた関数がコンパイルされ Appでの非同期/待ち受けの 最初の使用が完了しました これでコンパイルして実行することができます 今回は 非同期に移行する際の 他のテクニックを見てみましょう requestAuthorization 関数を見てみましょう saveとは異なり この関数は 完了ハンドラを受け取ります completionHandler バージョンを維持したまま非同期に なるように2つ目のバージョンの 関数を作成するつもりです そうすれば 完了ハンドラで 呼び出しているコードの他の部分は 動作を続けることができます これを簡単に行うにはリファクタリング操作 「Create Async Alternative」を使います この操作はCommand+Shift+Aで 表示される「Code Action Menu」 メニューにあります そして 非同期の代替手段を 追加するオプションを選択しました これにより 元のコードから2つ目のバージョンの 関数が追加され 元の完了ハンドラのコードは 新しい非同期タスクを作成して 新しい非同期バージョンの関数を 呼び出すだけのコードに置き換えられています リファクタリング操作に非推奨の警告が 追加されているのにご注意下さい これにより コードの一部を リファクタリングして 新しい 非同期バージョンを呼び出すことが 有益であることが分かるようになります さて 元に戻して completionHandler バージョンに戻ってみましょう requestAuthorization 関数内では コールバックは 任意のスレッドで 発生させることができます だからその中のコードがスレッド セーフなのを知る必要があります しかし 私はそうは思いません このisAuthorizedへの割り当ては 他のスレッドでこの値を読む コードと同時に発生する可能性があります また この後のコードにもローカルな 推論が欠けている例があります completionHandlerが 呼ばれていますが このコードがスレッドセーフか どうかは分かりません この完了ハンドラのコードが スレッドセーフかどうかは この関数の呼び出しサイトを見てみないと 分かりません リファクタリングされたバージョン を見るため 再度やってみましょう ここで覚えておいてほしいのは 新しい非同期タスクも 任意のスレッドで実行されるということです そのためこの転送バージョンには 完了ハンドラ版と同様の問題があります 私たちはまだコードを安全にしていませんが 悪くもしていません まもなくアクターを導入して この問題を解決する予定です しかし 今のところこの関数を 非同期に変換したからといって レースコンディションから 解放されるわけではないことに 注意する必要があります 実際 非同期関数を導入するため だけにリファクタリングを行うと コードに新たな競合状態が発生する 危険性があることを知っておく必要があります さて この新しい非同期関数を見てみましょう リファクタリングによって完了ハンドラは 既に requestAuthorizationの 新しい非同期バージョンへの 呼び出しに変換されています しかし この関数を非同期に変換することで 興味深いことが浮き彫りになりました ここでは 完了ハンドラはの テクニックを使っているときに 完了ハンドラを呼び出さずにリターンしていました これはおそらくバグです 呼び出し側は そのまま 宙ぶらりんになっていたでしょう しかし 非同期関数では 値を返さなければならないので このバグは実際にはありえません そこで このエントリを 失敗したことを示す「偽」を 返すように置き換えることができます 先ほどと同じように 新しい非同期バージョンの requestAuthorizationは 成功のブール値を返しません 成功するかスローするかだけです そのため成功した場合には「真」を 失敗した場合には「偽」を返す必要があります これで プロジェクト全体が コンパイルされるようになりました なぜなら 他の場所にある古いコードでも 非同期バージョンに転送されるだけの 完了ハンドラバージョンを呼び出せるからです そして 将来のリファクタリングに ついて教えてくれる 非推奨の警告が残されています さて もう1つの非同期変換は HealthKitからデータを読み込む関数です これは少し厄介です 前回と同じように 古いコードを 呼び出すためのスタブを作ります
その後 非同期バージョンに移行します ちなみにこのバージョンでは オプションで完了ハンドラを使用し 破棄可能な結果に類似した 非同期関数を使用します
そして 完了ハンドラのコードを 全て置き換えていきます
しかし ここまで来ると HealthKitのクエリAPIが どのように配置されているかに 関係して障害が発生します 完了ハンドラは ここではクエリタイプにあります しかし 実際に待ち受けたいのは この下のクエリの実行です ちなみに このように関数の中を 行ったり来たりすることも async/awaitが解決してくれる 優れた点です そこで クエリの作成と実行を行う単一の 非同期関数を作成したいと思います そのためにはcontinuation(継続) と呼ばれるテクニックを使います それでは ここに移動して ヘルパー関数を作成してみましょう 既存の関数の中ですべてを行うこともできますが ちょっと面倒なので 別のヘルパー関数にしておきたいと思います この関数はHealthKitに 問い合わせをします 非同期なので待ち受けることができますが クエリ操作が失敗するとスローされます そしてこの関数はここの完了ハンドラから 通常に返される有用な値を 返すことになります それでは クエリコードを 下の方まで移動して ヘルパー関数に移動します そして クエリの実行を 下から取ることにしました さて このコードを反転させて 完了ハンドラを待ち受け 非同期関数からこれらの値を 返すようにする必要があります そこで 継続を使うのです
この関数の最初に withCheckedThrowingContinuationの 呼び出しを待った 結果を返すことにします これは 再開に使用する継続を取る ブロックを取ります そして 実行したい コードをすべて その継続に移動させます そして その継続を使って 完了ハンドラを置き換えます
・・・失敗した場合はエラーを投げるか・・・
再開して返す・・・
・・・これらの便利な値を返します
さて この待ち受け可能な関数を手に入れたので これをもとのコードで使うことができます つまり 新しいクエリを待つ 結果を割り当てるだけです そして エラーを処理したいので do-catchブロックの中に 入れています キャッチの中では 実際にヘルパー関数からこのコードを 引き出したいと考えています そこで 失敗を記録します そして 「偽」を返します 残りのコードを ハッピーパスに移動させます
最後に まだ完了ハンドラを 使用しているこのクロージャーに 対処する必要があります ここでは DispatchQueue.main.asyncを使って メインスレッドに戻っています しかし 完了ハンドラを捨ててしまったので この情報をフォームを使って呼び出し側に 伝える方法がありません 代替手段が必要です これを解決するためにアクターを 始めて使うことになります Swiftのコンカレンシーモデルでは メインスレッド上のすべての操作を 調整するメインアクターと 呼ばれるグローバルアクターがあります ディスパッチのmain.asyncを MainActorの実行関数の 呼び出しに置き換えれます
これはMainActorで実行 するコードのブロックを受けます runは非同期なので これを待ち受ける必要があります 待機が必要なのはメインスレッドが この操作を処理する準備ができるまで この関数を一時停止する必要があるからです しかし 待機させているので completionHandlerの 使用をやめて 代わりにこのブロックが完了したら 単に「真」を返せばいいのです
次に コンパイラがキャプチャされた変数に関する エラーを出しています これは非同期関数内でのみ発生する 新しいエラーです Swiftのクロージャは参照に よって変数をキャプチャするので 変更可能な変数(この場合は newDrinks配列)を キャプチャすると-- ミュータブルな状態を共有する可能性があり それが競合状態の原因となることがあります 1つの方法はクロージャのキャプチャリストに newDrinksを追加することです 1つの方法はクロージャのキャプチャリストに newDrinksを追加することです そうすると自動的に不変のコピーが作成され クロージャ内で使用できるようになります しかし そもそも可変型の変数を 持たないようにして この問題を回避した方が良い場合もあります ここでは newDrinksを 変更してこれを実現しています このように書かれているのは completionHandlerの オプション性によるものです しかし 代わりにletにして elseのパスで空の値を代入すれば・・・
・・・この問題は解決します letで宣言された値の型は不変なので クロージャ間で共有しても全く問題ありません メインアクターの話を続けるために メインスレッドで実行する必要のある この関数を見てみましょう 操作メニューを表示し 定義にジャンプを選択します そして この関数の先頭には 実に素晴らしいアイデアがあります それは この関数がメインスレッド上で正しく 実行されていることを示します もし間違えてこの関数をメインスレッドへの 非同期ディスパッチで ラップせずに呼び出してしまったら デバッグビルドでエラーが発生するため 既存のコードの一部には この方法を採用する必要があります しかし この方法には いくつかの制限があります 必要な場所にアサーションを配置することを 忘れる可能性があり 保存されたプロパティへのアクセス についてアサーションを行うことは できませんし 少なくとも 多くの定型文がなければできません コンパイラがこれらのルールの1部を 強制することができれば このようなミスを全く犯すことができなくなります これがメイン・アクターの使い方です 関数に@MainActorの アノテーションを付けれます
そして 関数が実行される前に 呼び出し側がMainActorに 切り替わることを要求しています これで アサーションを削除することができます なぜなら コンパイラはこの関数が メインスレッド以外で呼び出される ことを許可しないからです これを証明するには呼び出しサイトに戻って MainActor.runの外に 呼び出しの1つを移動します すると コンパイラが 「ダメだ MainActor ではないからここからは呼べない」 と言うのが分かります この機能については 次のように考えることができます 「これはオプションの値によく似ている」 以前はポインタのような値を持っていて nilをチェックすることを覚えて いなければなりませんでしたが それは簡単に忘れてしまうものでした それよりも コンパイラがこのチェックを 常に行うようにし それを簡単にするための言語構文上の 工夫をする方がずっと良いのです ここでは似たようなことをしていますが nilのチェックを強制する代わりに どのアクターで実行しているかを強制しています この関数をMainActorに 配置したことで 厳密には MainActor.runはもう必要ありません アクターの外にいても 待ち受けることでそのアクター上の 関数をいつでも実行できます 実際 ここではコンパイラが そう教えてくれています つまり fix-itを受け入れて updateModelをawaitできるのです ここでは 同期関数にawaitを使用しています updateModelは同期しています が awaitは 私たちが入って いる関数がMainActorに 入るために一時停止する必要が あることを示しています DispatchQueue.syncの 呼び出しに似ていると 考えてください ただし awaitの場合 関数はブロックするのではなく 一時停止し メインスレッドへの 呼び出しが完了した後に再開します 従って ここではもう必要ありませんが このMainActor.runテクニックは 別の理由でまだ重要です awaitの度に あなたの関数は中断され 他のコードが実行されるかもしれません スレッドをブロックするのではなく 他のコードを実行させることが awaitingの目的です 今回のケースでは waitする 関数は1つだけなので それは問題ではなく このMainActor.run テクニックも必要ありませんが 複数の呼び出しをメインスレッドで実行したい 場合もあります 例えば テーブルビューの エントリを更新するような UIの更新作業をしている場合 実行する操作の間にメインのランループを 回したくない場合があります その場合MainActor.runを 使用してMainActorへの 複数の呼び出しをグループ化し それぞれの呼び出しが中断することなく 実行されるようにします これで メインスレッドで実行する 必要のあるコードを MainActorで保護する ことができるようになりました しかし このクラスの他のコード 特に先程のクエリアンカーのように ローカル変数を変更するコードはどうでしょうか? コードがレースコンディションに 陥るのをどう防げるでしょうか? 1つの方法は HealthKitControllerの 全体をメインアクターに 置くことです
個々の方法ではなく クラスのここにMainActorと書けば メインスレッドで調整された このタイプの全ての方法と プロパティが保護されます このようなシンプルなAppでは おそらく問題のない選択でしょう しかし それも少し間違っているような気がします このHealthKitControllerは 本当にAppのバックエンドです メインスレッドで全ての作業を 行う必要はないと思います そのスレッドには UIに特化した 活動を行うための 自由を与えておきたいのです そこで代わりに このクラス自体を アクターに変更することができます グローバルなアクターである MainActorとは異なり このアクタータイプは複数回 インスタンス化することができます 今回のプロジェクトでは まだ1つしか作成しませんが 同じアクターのコピーを複数作成するような アクターの使い方は 他にもたくさんあります 例えばチャットサーバーの各部屋を それぞれのアクターにすることができます さて このクラスをアクターにしたところで コンパイラが何を言うか見てみましょう OKです いくつかのコンパイル エラーが発生していますね ここで一息ついて コンパイラ エラーについて話しましょう これらのエラーは コードを 新しい並列モデルに移行する際に 更新しなければならないコードの場所を 示しています これらのエラーが発生した場合に その内容を理解するようにしてください 問題を解決する方法や理由が分からない場合は 修正ボタンを押したくなる誘惑に 負けないようにしましょう 注意しなければならないのは エラーの連鎖に巻き込まれることです 時々 ある変更を行うことがあります 先ほどのようにクラスをアクターに変換したり メソッドを非同期にしたりといった変更を行うと コンパイラエラーが発生することがあります エラーが発生した場所に行くと そのエラーを修正するために メソッドを非同期にしたり MainActorに配置したりと 更に変更を加えたくなるものです しかし これでは更にエラーが増えてしまい すぐに気持ちが萎えてしまいます その代わり このチュートリアルで 紹介しているような テクニックを使って 変更を隔離しプロジェクトの コンパイルと実行を挟んで 1つずつ行うようにしましょう 後で削除することになっても 古いコードがそのまま使えるように シムを追加します そうすることで ある時点から 徐々にコードを整理しながら 進めていくことができます ちなみに 私がここでやったことは まずHealthKitControllerの メソッドを非同期に変換して それをアクターにしました アクター化から始めるのではなく そのようにした方が一番楽になると思います では これらのエラーを見てみましょう MainActorに配置した 関数上にあり これは理にかなっています この関数では 新しい HealthKitController アクターの保存されたプロパティ モデルプロパティを触っています アクターはその状態を保護し アクター以外の関数がその状態に アクセスすることを許しません 例えば この保存されたプロパティなどです さて この関数を見てみると アクターの状態で触れるのは モデルオブジェクトだけのように見えます それ以外はすべて関数の引数として渡されます 私にとっては これは 実際にこの関数がモデルに属することを 示唆しています では そうしましょう HealthKitcontrollerから このメソッドを切り出し CoffeeDataモデルに移動して 貼り付ければいいのです これは HealthKitControllerから 呼び出すパブリックメソッドになります そして モデルへの参照をすべて 置き換える必要がありますが それはselfになります
最後に 呼び出し元のサイトに 戻る必要があります・・・
・・・代わりにモデルの このメソッドを呼び出します これでHealthKitControllerが コンパイルされましたが 他のファイルから新なエラーが発生しています ここでは 先程追加した完了ハンドラシムを 呼び出しています これらの関数は アクターへの 呼び出しを持つタスクを作成するだけなので 実際には アクターでの分離は 必要ありません アクターの状態の他の部分には一切触れていません そのため いわゆる「非分離」と マークすることができます
これは アクターの保護状態の一部 ではないことをコンパイラに伝えるもので コードの他の部分から直接呼び出すことができます そのためこれをnonisolated としてマークする必要があります
これで プロジェクトがコンパイルされました コンパイラはこの非分離型の主張が 正しいかどうかをチェックする ことに注意してください もし私がアクターの状態にアクセスしようとすると 例えば認識状態をプリントアウトしようとすると
コンパイルエラーが発生します 以上で HealthKitControllerを アクターに変換し 内部状態を競合状態から 保護する作業が完了しました 次に 非推奨の警告に従って 次に作業したい場所 つまりCoffeeData モデルタイプに進みます このクラスはObservableObjectを 実装しており @Publishedプロパティを持っています SwiftUIビューに公開されている プロパティの更新は メインスレッドで行わなければならないので このクラスはMainActorに 配置するのに 適した候補と言えます しかし ここにはバックグラウンド で作業を行うための DispatchQueueもあります これがどこで使われているか見てみましょう
読み込みと保存の2つの関数だけです これは理にかなっています 恐らく読み込みと保存をメイン スレッドでは行いたくないでしょう このようなパターンを目にしたら キューは 特定の活動を調整するために 使用されているが クラスの 残りの部分はメインスレッド である必要があるということです これは バックグラウンドの コードを別のアクターに移す 必要があることを示しています では やってみましょう
このファイルの先頭に行って 「CoffeeDataStore」という 新しいプライベートアクターを作成します
そして 別のエディタウィンドウで CoffeeDataを開きます まず この新しいアクターに コードを移そうと思います まず このアクターに独自のロガーを与えます
これを更新します これにより コードを実行した時に 新しいアクターを使用している ことが分かるようになります 次に このバックグラウンドの DispatchQueueの 代わりに 新しいアクターの インスタンスを使用します
まず 保存操作に進み これを取り出してアクターに移動させます ここからカットします そして コンパイルして どんな 問題が発生するか見てみましょう まず currentDrinksです これは このメソッドをアクターに移す前は モデルタイプのプロパティでした では どうやってアクセスすれば いいのでしょうか? さて アクターが情報をリレーする方法は お互いに値を渡すことです そこで この関数にcurrentDrinksを 引数として渡すようにします・・・
・・・モデルはそれをアクターの 関数呼び出しに渡します これで解決ですね 次に savedValueです これは最後に保存された値のコピーで 何も変わっていないのに不必要に保存することを 避けるためのものです この値はsave関数とload関数の 両方で変更されるので アクターによる保護が必要です というわけで アクターに移動させてみましょう
さて 次です このプロパティdataURLは 実際には読み込みと保存の操作で 使用されるだけなので これもアクターに移すだけです
OK 最後に解決すべき問題があります ここではエラーが発生しています 見てみると アクターの状態を取り込んでいる クロージャーがあるようですので これを修正する必要があります さて なぜここにクロージャーが あるのでしょうか? 下を見てみると 同じコードが2か所で使われているからです そして これは実際にコンパイラがフラグを立てた 問題であることが分かりとても興味深いものでした このコードが行っているのはwatch拡張機能が 現在バックグラウンドで実行されて いるかをチェックすることです もし既にバックグラウンドで 実行されているのであれば バックグラウンドのキューに入ることなく メインスレッドに留まり 同期的にタスクを実行するという考え方です しかしこれは正しいとは思えません Appがバックグラウンドで動作している場合でも メインスレッドをブロックして同期IO操作を 実行してはいけません なぜAppはこのようなことをするのでしょうか? それは save操作がどこで 呼び出されているかによります
これは この下にあるcurrentDrinks プロパティのdidSetから呼び出されています これにより プロパティが割り当てられる度に 新しい値が保存されます さて didSetsは非常に便利ですが ちょっと誘惑が強すぎるかもしれません currentDrinksプロパティの 呼び出し元を全てみてみましょう ここでずっと掘り下げていくと 保存操作は同期的に行われていることが分かります その呼び出し方法はWatchKit拡張の バックグラウンドタスク用の ハンドラに由来しているからです さて このハンドルAPIには契約があります 全ての作業を終えてからこの setTaskCompletedWithSnapshot メソッドを 呼び出すことになっています そして このメソッドを呼び出した時点で 全ての作業が終わっていることを 保証しなければなりません その時点でウォッチAppは サスペンドされるからです 終わったと言ったときに 保存操作のようなI/O操作が まだ実行されていてはいけません これは 非同期性がコード全体に グローバルな推論を 強いることを示す完璧な例です ここで何が起こっているのかを 視覚化してみましょう まず handle(backgroundTasks:)で HealthKitから 読み込み機能を呼び出しています これは 完了ハンドラを受け取ります しかしその後 同期的に実行される updateModel()に切り替わるので 同期的にdidSetを呼び出し 同期的に保存されることになります これが終わると 完了ハンドラが 呼ばれ WatchKitに 完了したことが通知されます 同期的な部分があるからこそメインスレッドで 同期的なI/Oを行わざるを得ないのです これをどうやって解決するのでしょうか? 完了ハンドラで修正するには 現在 同期的なメソッドを それぞれアップデートして 完了ハンドラを取るようにしなければなりません しかし didSetでは そのようなことはできません didSetは引数を取らず プロパティを更新すると 自動的に実行されます そこでまず 公開されたプロパティに行って private(set)にしてみましょう 次に didSetの操作を行い このロジックを新しい
drinksUpdated関数に 移動させましょう
この関数は非同期にします
この関数の中では 新しいアクターの保存操作の呼び出しを待ち・・・
新しいcurrentDrinksの値を渡します そしてcurrentDrinksが 更新される場所に行き drinksUpdatedを 確実に呼び出す必要があります
ここで最後に注意すべきことがあります currentDrinksのコピーを作成し それを変異させてから書き戻すというこの操作は 1つのアトミックな操作として 行われることが重要です awaitキーワードが非常に 重要な手がかりとなるのは このためでコードのこの時点で この操作が中断され 他の操作 (currentDrinksを 更新する可能性のある操作)が 実行される可能性があることを示しています そのため await可能な操作を 呼び出す前に値を取得して変異させ それを格納するという更新が完了していることを 確認する必要があります これで完了ですこれで保存操作に戻り リファクタリングを完了することができます・・・
・・・このアクターを変更してすべての操作を バックグラウンドで行うようにします
最後に読み込み操作を見てみましょう 再度エディタを表示してみましょう
ここでは バックグラウンドで 実行する必要のあるコードと メインスレッドに戻って実行する 必要があるコードの間でロジックが 分割されています そこで 関数の一番上の部分 (ここまで)を持ち上げて アクターの中に移動させます
ここで別のレースコンディションが 発生する可能性に気づきました ここのsavedValuesは メインのキューで変更されて いましたが保存操作を覚えている場合は バックグラウンドキューで 読み取りと書き込みが行われました さて たまたまAppの構造上 読み込みは起動時にしか行われない ので これは問題ありませんでした しかし これは大局的な推論に頼っており 将来的に変更を加えたときに 微妙に破綻する可能性のある仮定です プログラムが常に正しいことを アクターに保証させる方がはるかに良いのです では この手動のキー管理を削除し インデントを修正して・・・
保存と同様に 読み込んだ値を戻す方法が必要ですが これは単にload関数から 値を返すことで行います
それでは モデルタイプの 読み込み操作に戻りましょう このコードは移動させたので 削除してしまいましょう
ここでやるべきことは I/Oアクターからデータを読み込むことです
ここでは 非推奨のメソッドを整理することができます
つまり この読み込みを非同期に する必要があります・・・
つまり この読み込みを 新しい非同期タスクから呼び出す必要があるのです
しかし この時点で 単に非同期タスクを使用した場合 新しい競合状態を引き起こす可能性があります アクターの外では新しい非同期タスクを作成すると 任意のスレッドで実行されることを 覚えておいてください currentDrinksのような共有状態を 任意のスレッドから変異させるべきではありません これを解決する1つの方法は このタスクをMainActorに置くことで このようにすることができます しかし もっと良い方法があります それは このモデルタイプ全体を MainActorに移すことです
その方法はCoffeeModelの定義で @MainActorをモデルタイプに追加します
モデルをMainActorに 置くことで CoffeeDataのプロパティへのアクセスは 全てメインスレッドから 行われることが保証されます これは 先程述べたように CoffeeDataは公開された プロパティを持つ ObservableObjectであり SwiftUIに公開されたプロパティは メインスレッドでのみ更新され なければならないことを意味します また アクターからの非同期の呼び出しも アクター上で実行されることになります この型をMainActorで保護しても 先ほどHealthKitControllerを アクターに移動させたときと違って コンパイルエラーにならなかったことに お気づきでしょうか? これは モデルに呼び出している 場所が SwiftUIのビューの ようなものだからです 例えば これはDrinkListViewです これはボタンのリストを表示し そのうちの1つをクリックすると addDrinkメソッドを呼び出し coffeeDataモデルを更新します このDrinkListView自体は MainActor上にあります そのため そのメソッドは待ち時間を必要とせずに MainActor上の私のモデル タイプを呼び出すことができます このSwiftUIビューが MainActor上にあることを 決定するものは何でしょうか? それはEnvironmentObjectの 使用から推測されます-- 共有状態にアクセスするすべての SwiftUIビューは 環境オブジェクトや観測されたオブジェクトなどは 常にMainActor上にあります 他の場所では以前に見たこの拡張デリゲートの 呼び出しからモデルにアクセスしています この拡張デリゲートは 常に メインスレッドで呼ばれることが 保証されているのでWatchKitによって MainActorで実行されて いるとアノテーションされており ここで直接モデルタイプを呼び出す ことができるようになっている 最後に ここまで来たら このメソッドをリファクタリング して この非推奨の完了ハンドラの 使用をやめましょう 代わりにこれを別の非同期タスクに まとめることができます
そうすれば HealthKitの 読み込みが戻ってきたときに 完了ハンドラはメインスレッドを ブロックすることなく 全ての作業が完了したことが分かり setTaskCompletedWithSnapshot を 呼び出すことができます これで 非同期操作を待ってから それ以上の作業を行うという 構造化された トップダウンアプローチができました ちなみに このような構造化されたアプローチは Swiftの並列機能のもう1つの重要な部分です さらに詳しく知りたい方は 関連する講演をご覧ください 例えば 複数の非同期操作が 完了するのを待ってから続行するような より複雑な例を構成するためにこの機能を 利用する方法を説明しています この講演をご覧になって これからの新機能がどのように動作するのか 疑問に思われた方は技術の詳細を説明した 「アンダーザフッドトーク」をご覧ください では おさらいしましょう しっかりした型のアーキテクチャでありながら 並列処理のアーキテクチャが複雑で 発見するのが難しい レースコンディションが隠れている コードを取り上げました そして 新しい並列機能の助けを借りて 並列処理と型のアーキテクチャが 上手く調和するように コードを再構築しました コンパイラは その過程で 隠れた潜在的なレース コンディションを見つけました Swift 5.5にはタスクグループを使った 構造化された並列処理非同期シーケンス SDKのいくつかの優れた新しい非同期APIなど まだ取り上げていないことがたくさんあります また 今回のプロジェクトでは行わなかった リファクタリングもいくつかあるので 自分で試していただくのもいいかもしれません これらのテクニックを学ぶ 1番の方法は 自分のAppで 試してみることなので よりクリーンで安全な コードの書き方を楽しんでください ♪
-
-
0:07:34 - Call the async version of the HKHealthKitStore save(_:) method
do { try await store.save(caffeineSample) self.logger.debug("\(mgCaffeine) mg Drink saved to HealthKit") } catch { self.logger.error("Unable to save \(caffeineSample) to the HealthKit store: \(error.localizedDescription)") }
-
0:09:38 - Change save(drink:) to be an async function
public func save(drink: Drink) async {
-
0:10:15 - Create a new asynchronous task
Task { await self.healthKitController.save(drink: drink) }
-
0:12:13 - Add an async alternative for requestAuthorization(completionHandler:)
@available(*, deprecated, message: "Prefer async alternative instead") public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) { Task { let result = await requestAuthorization() completionHandler(result) } }
-
0:14:55 - Update the async version of requestAuthorization()
public func requestAuthorization() async -> Bool { guard isAvailable else { return false } do { try await store.requestAuthorization(toShare: types, read: types) self.isAuthorized = true return true } catch let error { self.logger.error("An error occurred while requesting HealthKit Authorization: \(error.localizedDescription)") return false } }
-
0:15:43 - Add an async alternative for loadNewDataFromHealthKit(completionHandler:)
@available(*, deprecated, message: "Prefer async alternative instead") public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) { Task { completionHandler(await self.loadNewDataFromHealthKit()) } }
-
0:17:43 - Create a queryHealthKit() helper function that uses a continuation
private func queryHealthKit() async throws -> ([HKSample]?, [HKDeletedObject]?, HKQueryAnchor?) { return try await withCheckedThrowingContinuation { continuation in // Create a predicate that only returns samples created within the last 24 hours. let endDate = Date() let startDate = endDate.addingTimeInterval(-24.0 * 60.0 * 60.0) let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [.strictStartDate, .strictEndDate]) // Create the query. let query = HKAnchoredObjectQuery( type: caffeineType, predicate: datePredicate, anchor: anchor, limit: HKObjectQueryNoLimit) { (_, samples, deletedSamples, newAnchor, error) in // When the query ends, check for errors. if let error = error { continuation.resume(throwing: error) } else { continuation.resume(returning: (samples, deletedSamples, newAnchor)) } } store.execute(query) } }
-
0:20:17 - Update the async version of loadNewDataFromHealthKit()
@discardableResult public func loadNewDataFromHealthKit() async -> Bool { guard isAvailable else { logger.debug("HealthKit is not available on this device.") return false } logger.debug("Loading data from HealthKit") do { let (samples, deletedSamples, newAnchor) = try await queryHealthKit() // Update the anchor. self.anchor = newAnchor // Convert new caffeine samples into Drink instances. let newDrinks: [Drink] if let samples = samples { newDrinks = self.drinksToAdd(from: samples) } else { newDrinks = [] } // Create a set of UUIDs for any samples deleted from HealthKit. let deletedDrinks = self.drinksToDelete(from: deletedSamples ?? []) // Update the data on the main queue. await MainActor.run { // Update the model. self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks) } return true } catch { self.logger.error("An error occurred while querying for samples: \(error.localizedDescription)") return false } }
-
0:25:09 - Annotate updateModel(newDrinks:deletedDrinks:) with @MainActor
@MainActor private func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) {
-
0:26:43 - Remove MainActor.run from the call site of updateModel(newDrinks:deletedDrinks:)
await self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks)
-
0:29:24 - Change HealthKitController to be an actor
actor HealthKitController {
-
0:32:31 - Move updateModel(newDrinks:deletedDrinks:) to CoffeeData
@MainActor public func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) { guard !newDrinks.isEmpty && !deletedDrinks.isEmpty else { logger.debug("No drinks to add or delete from HealthKit.") return } // Remove the deleted drinks. var drinks = currentDrinks.filter { deletedDrinks.contains($0.uuid) } // Add the new drinks. drinks += newDrinks // Sort the array by date. drinks.sort { $0.date < $1.date } currentDrinks = drinks }
-
0:33:18 - Update the call site of updateModel(newDrinks:deletedDrinks:)
await model?.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks)
-
0:34:01 - Mark the deprecated completion handler methods as nonisolated
@available(*, deprecated, message: "Prefer async alternative instead") nonisolated public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) { // ... } @available(*, deprecated, message: "Prefer async alternative instead") nonisolated public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) { // ... }
-
0:36:20 - Create a private CoffeeDataStore actor for loading and saving
private actor CoffeeDataStore { }
-
0:36:43 - Add a dedicated logger for CoffeeDataStore
let logger = Logger(subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.CoffeeDataStore", category: "ModelIO")
-
0:37:05 - Add an instance of the actor to CoffeeData
private let store = CoffeeDataStore()
-
0:38:37 - Move the savedValue property from CoffeeData to CoffeeDataStore
private var savedValue: [Drink] = []
-
0:39:00 - Move the dataURL property from CoffeeData to CoffeeDataStore
private var dataURL: URL { get throws { try FileManager .default .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) // Append the file name to the directory. .appendingPathComponent("CoffeeTracker.plist") } }
-
0:42:42 - Move the didSet for currentDrinks to a new async function
@Published public private(set) var currentDrinks: [Drink] = [] private func drinksUpdated() async { logger.debug("A value has been assigned to the current drinks property.") // Update any complications on active watch faces. let server = CLKComplicationServer.sharedInstance() for complication in server.activeComplications ?? [] { server.reloadTimeline(for: complication) } // Begin saving the data. await store.save(currentDrinks) }
-
0:44:00 - Update addDrink(mgCaffeine:onData:) to call drinksUpdated()
// Save drink information to HealthKit. Task { await self.healthKitController.save(drink: drink) await self.drinksUpdated() }
-
0:44:09 - Update updateModel(newDrinks:deletedDrinks:) to call drinksUpdated()
await drinksUpdated()
-
0:44:17 - Mark the updateModel(newDrinks:deletedDrinks:) method as async
@MainActor public func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) async {
-
0:45:26 - Complete the move of the save() method into CoffeeDataStore
// Begin saving the drink data to disk. func save(_ currentDrinks: [Drink]) { // Don't save the data if there haven't been any changes. if currentDrinks == savedValue { logger.debug("The drink list hasn't changed. No need to save.") return } // Save as a binary plist file. let encoder = PropertyListEncoder() encoder.outputFormat = .binary let data: Data do { // Encode the currentDrinks array. data = try encoder.encode(currentDrinks) } catch { logger.error("An error occurred while encoding the data: \(error.localizedDescription)") return } // Save the data to disk as a binary plist file. do { // Write the data to disk. try data.write(to: self.dataURL, options: [.atomic]) // Update the saved value. self.savedValue = currentDrinks self.logger.debug("Saved!") } catch { self.logger.error("An error occurred while saving the data: \(error.localizedDescription)") } }
-
0:46:20 - Move the top part of the load() method into CoffeeDataStore
func load() -> [Drink] { logger.debug("Loading the model.") var drinks: [Drink] do { // Load the drink data from a binary plist file. let data = try Data(contentsOf: self.dataURL) // Decode the data. let decoder = PropertyListDecoder() drinks = try decoder.decode([Drink].self, from: data) logger.debug("Data loaded from disk") } catch CocoaError.fileReadNoSuchFile { logger.debug("No file found--creating an empty drink list.") drinks = [] } catch { fatalError("*** An unexpected error occurred while loading the drink list: \(error.localizedDescription) ***") } // Update the saved value. savedValue = drinks return drinks }
-
0:48:01 - Update the load() method in CoffeeData to use the actor
func load() async { var drinks = await store.load() // Drop old drinks drinks.removeOutdatedDrinks() // Assign loaded drinks to model currentDrinks = drinks // Load new data from HealthKit. let success = await self.healthKitController.requestAuthorization() guard success else { logger.debug("Unable to authorize HealthKit.") return } await self.healthKitController.loadNewDataFromHealthKit() }
-
0:49:08 - Update the CoffeeData initializer to use an async task
Task { await load() }
-
0:50:03 - Annotate CoffeeData with @MainActor
@MainActor class CoffeeData: ObservableObject {
-
0:52:18 - Replace the completion handler usage in the handle(_:) method of ExtensionDelegate
// Check for updates from HealthKit. let model = CoffeeData.shared Task { let success = await model.healthKitController.loadNewDataFromHealthKit() if success { // Schedule the next background update. scheduleBackgroundRefreshTasks() self.logger.debug("Background Task Completed Successfully!") } // Mark the task as ended, and request an updated snapshot, if necessary. backgroundTask.setTaskCompletedWithSnapshot(success) }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。