ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftの並行処理: 舞台裏
Swiftの並行処理の詳細を深く掘り下げて、データレースやスレッドの爆発が生じるリスクを低減すると同時にパフォーマンスを向上させるために、Swiftがどのような役割を果たすのかを明らかにします。SwiftのタスクのGrand Central Dispatchとの相違点、新しい協調スレッドモデルのしくみ、そしてAppの最高のパフォーマンスを確保する方法を探ります。 このセッションを最大限活かしていただくためには、事前に「Swiftのasync/awaitについて」、「Swiftにおける構造化並行処理」、「Swiftアクターによるミュータブルステートの保護」を先にご確認いただくことをお勧めします。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
WWDC17
WWDC16
-
ダウンロード
♪ (Swiftの並行処理: 舞台裏) こんにちは 「Swiftの並行処理: 舞台裏」へようこそ 私の名前はRokhini Prabhu Darwin Runtimeチームに勤務しています 今日私の同僚のVarunと私は Swiftの並行処理に関する 根本的なニュアンスのいくつかについて お話することにとてもワクワクしています
これはSwiftの並行処理:Concurrency について 以前の講演を踏まえた上級者向けの講演です async/await 構造化並行処理 およびアクターの概念に精通していない場合は こちらのお話をまず お聞きになることをお勧めします Swift並行処理に関する以前の講演で 今年Swiftに備わるさまざまな 言語機能およびその使用方法に ついて学習しました この講演では言語の安全性だけでなく パフォーマンスと効率性のために これらの基本要素がそのように 設計されている理由をより深く理解します ご自身のAppでSwiftの並行処理を 実験して採用しながら この講演でSwiftの並行処理について推論する方法と Grand Central Dispatchなどの 既存のスレッドライブラリとの インターフェース方法についての より良いメンタルモデルを 提供することを望んでいます 今日はいくつかの点についてお話します 最初にSwiftの並行処理の背後にある スレッドモデルについて説明し Grand CentralDispatchと対比します 並行処理言語機能を利用して Swiftの新しいスレッドプールを構築し それによってパフォーマンスと効率を 向上させる方法について説明します 最後に このセクションでは コードをSwiftの並行処理を 使用するように移植するときに 留意する必要がある考慮事項について説明します 次にVarunがアクターを介した Swift並行処理の同期について説明します アクターが内部でどのように機能するか シリアルディスパッチキューなど すでに使い慣れている既存の同期基本要素と どのように比較するか そして最後に アクターを使用してコードを 作成するときに留意すべき点について説明します 今日はお話しすることが多く あるのですぐ始めましょう スレッドモデルに関する 今日のディスカッションでは Grand Central Dispatchなどの 今日利用可能なテクノロジーで記述された サンプルAppを確認することから始めます 次にSwiftの並行処理で書き直したときに 同じAppがどのように機能するかを確認します 自分のニュースフィードリーダーAppを 書きたいとしましょう 私のAppの高レベルコンポーネントが 何であるかについて話しましょう 私のAppにはユーザーインターフェイスを 駆動するメインスレッドがあります ユーザーが登録しているニュースフィードを 追跡するデータベースがあり 最後にフィードから最新の コンテンツを取り込むための ネットワークロジックを処理する サブシステムがあります Grand Central Dispatchキューを使用して このAppを構成する方法を考えてみましょう ユーザーが最新のニュースを 見ることを要求したとしましょう メインスレッドではユーザー イベントジェスチャを処理します ここからデータベース操作を 処理するシリアルキューに dispatch_async() をリクエストします この理由は2つあります まず作業を別のキューにディスパッチすることで 大量の作業が発生するのを待っている間でも メインスレッドがユーザー入力に 応答し続けることができるようにします 次にシリアルキューが相互排斥を保証するため データベースへのアクセスが保護されます データベースキューにいる間 ユーザーがサブスクライブしたニュースフィードを 繰り返し処理しそれぞれについて URLSessionへのネットワークリクエストを スケジュールしてそのフィードの コンテンツをダウンロードします ネットワークリクエストの結果が届くと 並行キューであるデリゲートキューで URLSessionコールバックが呼び出されます 各結果の完了ハンドラーで これらの各フィードからの 最新のリクエストでデータベースを 同期的に更新し将来の使用のために データベースをキャッシュします 最後にメインスレッドを起動して UIを更新します これはそのようなAppを 構築するための完全に合理的な方法のようです リクエストの処理中にメインスレッドを ブロックしないようにしました またネットワークリクエストを 同時に処理することで プログラムに固有の 並列処理を利用しました ネットワークリクエストの結果を処理する 方法を示すコードスニペットを 詳しく見てみましょう まずニュースフィードからダウンロードを 実行するためのURLSessionを作成しました ここでお分かりのように この URLSession の delegateQueue を concurrentQueue に設定しました 次に更新が必要なすべてのニュースフィードを 繰り返し処理しそれぞれについて URLSession で dataTask をスケジュールします デリゲートキューで呼び出される dataTask の 完了ハンドラーでダウンロードの結果を 逆シリアル化し 記事にフォーマットします 次にフィードの結果を更新する前に データベースキューに対して dispatch_sync() を実行します ここではかなり簡単なことを行うために いくつかの直列的なコードを 記述したことがわかりますが このコードにはいくつかの隠れた 出力上の落とし穴があります これらのパフォーマンスの問題に ついてさらに理解するには まずGCDキューでの作業を処理するために スレッドを起動する方法を 掘り下げる必要があります Grand Central Dispatchでは 作業がキューに入れられると システムはその作業項目を 処理するためのスレッドを起動します 並行キューは一度に複数の 作業項目を処理できるため すべてのCPUコアが飽和状態になるまで システムは複数のスレッドを起動します ただしスレッドがブロックされ ここの最初のCPUコアで見られるように 並行キューで実行する作業がさらにある場合 GCDは残りの作業項目をさばくために より多くのスレッドを起動します この理由は2つあります まずプロセスに別のスレッドを 与えることで各コアがいつでも 作業を実行するスレッドを 持ち続けることを保証できます これによりAppに継続的レベルの 並行処理が提供されます 次にブロックされたスレッドはさらに進行する前に セマフォなどのリソースを 待機している可能性があります 作業を継続するために立ち上げられた 新しいスレッドキューにあると 最初のスレッドが待機しているリソースの ブロックを解除できる場合があります GCDでのスレッドの起動について 少し理解できたので 戻ってニュースAppでの コードのCPU実行を 見てみましょう Apple Watchのような2コアデバイスでは GCDは最初に2つのスレッドを起動して フィードの更新結果を処理します スレッドがデータベースキューへの アクセスをブロックすると ネットワークキューでの作業を継続するために さらに多くのスレッドが作成されます 次にCPUはさまざまなスレッド間の 白い縦線で示されているように ネットワーク結果を処理する さまざまなスレッド間で コンテキストスイッチを実行する必要があります これは当社のニュースAppでは 非常に多くのスレッドが発生する 可能性があることを意味します ユーザーが更新する必要のある100のフィードを 持っている場合 ネットワーク要求が完了すると それらの各URLデータタスクの 同時実行キューに完了ブロックができます GCDは各コールバックがデータベースキューで ブロックされるとより多くの スレッドを起動するため Appに多数のスレッドが含まれることになります あなたは当社のAppに たくさんのスレッドがあることの 何がそんなに悪いのかと疑問に思うかもしれません Appに多数のスレッドがあるということは CPUコアよりも多くのスレッドでシステムが オーバーコミットされていることを意味します 6つのCPUコアを搭載した iPhoneを考えてみましょう ニュースAppに処理が必要な100の フィード更新がある場合 これは コアの16倍のスレッドで iPhoneをオーバーコミットしたことを意味します これは私たちが「スレッド爆増」と呼ぶ現象です 以前の WWDCで App内でのデッドロックの可能性を含む 関連するリスクについての詳細について お話ししました スレッド爆増ではメモリ及び スケジューリングのオーバーヘッドも発生し すぐには顕在化しない場合があります これをさらに詳しく見ていきましょう 当社のニュースAppを振り返ると ブロックされた各スレッドが 貴重なメモリとリソースを保持したまま 再び実行するのを待っています ブロックされた各スレッドには スレッドを追跡するためのスタックと 関連するカーネルデータ構造があります これらのスレッドの一部は 実行中の他のスレッドが必要とする ロックを保持している可能性があります これは待機スレッドが占有する 多数のリソースとメモリです スレッド爆増の結果による より大きなスケジューリングコストもあります 新しいスレッドが立ち上がると同時に CPU が新しいスレッドを実行し始めるために 古いスレッドから切り替えるため 完全なスレッドコンテキストスイッチを 行う必要があります ブロックされたスレッドが再び実行可能になると スケジューラーはCPU上の スレッドをタイムシェアして すべてのスレッドが進行できる ようにする必要があります スレッドのタイムシェアリングは それが数回発生する場合は問題ありません これが並行処理の力です ただしスレッド爆増がある場合 限られたコア数のデバイスで 数百のスレッドをタイムシェアすると 過度のコンテキストスイッチングが 発生する場合があります スレッドのスケジューリングの待ち時間が それらが実行する 有用な作業の量に勝るため CPUの実行効率も低下します 私たちがこれまで見た通りGCDキューで Appを作成する際 これらのスレッドに関する 微妙な違いを見逃しがちであるため パフォーマンスが下がり オーバーヘッドが大きくなります この経験を踏まえ Swift は 言語への並行処理を設計する際 異なるアプローチを行いました 当社はSwiftの並行処理をパフォーマンスと効率にも 気を配って構築したため Appは制御可能で 構成化され安全な並行処理を享受できます SwiftではAppの実行モデルを スレッド数とコンテキストスイッチングが多い 現在のモデルから これに変更したいと考えています こちらでは 2コアシステムで実行する場合 スレッドは2本しかなく スレッドのコンテキストスイッチがありません ブロックされたスレッドは全て無くなり 代わりに作業の再開をトラッキングするための continuation「続行」として知られる 軽量オブジェクトがあります Swift並行処理の下でスレッドが作業を実行する際 完全なスレッドコンテキストスイッチングを 行う代わりに continuationの間で切り替えます これは代わりに関数呼び出しのコストのみを 支払うという意味です そのためSwift並行処理で求めるランタイムの動作は CPU コアと同じ数のスレッドのみを作成し スレッドがブロックされた時は ワークアイテムを安価かつ効率的に 切り替えることができるようにすることです 皆様には推論が容易で安全で制御された 並行処理を提供する直線的なコードを 書けるようになっていただきたいと思います 私たちが求めるこの動作を実現するために オペレーティングシステムには スレッドがブロックせず 言語がそれをもたらすことが できる場合のみに可能な 実行時の取決めが必要です Swiftの並行処理モデルと その周辺のセマンティクスは この目標を念頭に置いて 設計されています ランタイムとのコントラクトを 維持できるようにする Swiftの2つの言語レベルの機能について 詳しく説明したいと思います 1つ目は await のセマンティクスに由来し 2つ目は Swiftランタイムでの タスクの依存関係の追跡に由来します 当社の事例のニュースAppで これらの機能を考えてみましょう これは 先ほどお話ししたニュースフィードの 更新結果を処理するコードスニペットです Swift並行処理プリミティブで記述した場合 このロジックがどのようになるか見てみましょう まずはヘルパー関数の 非同期実行の作成から始めます その後並行処理ディスパッチキューで ネットワークリクエストの結果を 処理する代わりに タスクグループを使用して 並行処理を管理します 本タスクグループにおいて 当社は 更新される必要がある タスクグループでは更新が必要なフィードごとに 子タスクを作成します 各子タスクは共有 URLSession を使用して フィードのURLからダウンロードを実行します その後ダウンロード結果をデシリアライズし それらを 記事にフォーマットし 最終的に 非同期関数を呼び出してデータベースを更新します ここで非同期関数を呼び出す際 await キーワードで注釈を付けます 「Swiftのasync/awaitについて」の講義で await は非同期の待機であることを学びました それは非同期関数からの結果を待っている間 現行のスレッドをブロックしません 代わりに関数は中断され スレッドは他のタスクを実行するために 開放されます これはどのように起こりますか? どのようにスレッドを放棄しますか? 私の同僚のVarunが これを可能にするために Swift ランタイムの内部で 何が行われるかを説明します ありがとう Rokhini 非同期関数の実装方法に飛び込む前に 非同期関数がどのように機能するかについて 簡単に復習しましょう 実行中のプログラムの全スレッドには それぞれ一つのスタックがあり それを使用して関数呼び出しの状態を保存します とりあえずひとつのスレッドに焦点を当てます スレッドが関数呼び出しを実行する際 新規フレームがスタックにプッシュされます この新規に作成されたスタックフレームは ローカル変数 リターンアドレス その他の必要な情報を格納するために 関数が使用します 関数が実行を終了してリターンすれば そのスタックフレームがポップされます では非同期関数を考えてみましょう スレッドが updateDatabase 関数から Feed の addメソッドを呼び出したとします この段階で最新のスタックフレームは 「add」用になります このスタックフレームには 「停止点」以降で不要な ローカル変数を保存します addの本体には「停止点」があり await とマークされています ローカル変数idとarticleは 定義された後すぐに forループの本体で使用され 間に中断ポイントはありません よって それらはスタックフレームに格納されます さらにヒープには2つの非同期フレームがあり 一つは updateDatabase 用で 一つは add 用です 非同期フレームには 停止ポイントで利用可能でなければならない 情報を保存します newArticles の引数は待機前に定義されていますが 待機の後にも使用できる必要があります これは add 用の非同期フレームが newArticles をトラックすることを意味します スレッドが実行を続行すると save 関数が実行を開始する際 add 用のスタックフレームが save 用のスタックフレームに置き換えられます 新しいスタックフレームを追加する代わりに トップのスタックフレームが置き換えられます これは 将来必要になる変数が 非同期フレームのリストに すでに格納されているためです save 関数は使用する非同期フレームも取得します 記事をデータベースに保存する間 スレッドが ブロックされる代わりに いくつかの有用な作業を行うことが できるほうが良いです save関数の実行が停止されるとします スレッド はブロックされる代わりに その他の作業を行うために再使用されます 一時停止ポイントで維持されるすべての情報は ヒープに格納されるため 後の段階で実行を継続するために使用できます この非同期フレームのリストが 継続 continuation の実行時の姿です しばらくして データベースリクエストが完了し 一部のスレッドが解放されたとします これは前と同じスレッドかもしれませんし 異なるスレッドかもしれません save 関数が このスレッドで 実行を再開するとします 実行が終了し ID をリターンすると save のためのスタックフレームは再び add のためのスタックフレームと入れ替わります その後スレッドは zip の実行を開始します 2つの配列のジップは同期の処理なので 新規のスタックフレームを作成します Swift の continue はオペレーティングシステムの スタックを使用するため 非同期および同期 Swift コードの両方とも 効果的に C および Objective-C を呼び出せます さらにC および Objective-C コードは 効果的に 同期 Swift コードを呼び出し続けることができます zip 関数が終了するとスタックフレームは ポップされ 実行が続行されます ここまで スレッドのリソースを解放して 他の作業を実行しながら 効率的な一時停止と再開を保証するために await がどのように設計されているか 説明してきました 次にRokhiniが2つ目の言語の機能である タスク間の依存性の実行時の追跡について 説明します ありがとう Varun 先ほど説明された通り 関数は待機時に continuation に分割でき 潜在的な停止点としても知られます この場合 URLSessionのdataタスクは非同期関数で その後の残りの作業は continuation です continuation は 非同期関数が完了した後でのみ 実行できます これは Swift 並行処理ランタイムにより トラッキングされる依存性です 同様にタスクグループ内で 親タスクが複数の子タスクを作成した場合 それらの子タスクのそれぞれは 親タスクが進行する前に 完了する必要があります これはタスクグループのスコープにより コードで表される依存性であるため Swift コンパイラとランタイムに 明示的に認識されます Swift ランタイムに認識されている 継続又は子タスクのみを 待機できます したがって Swiftの並行処理プリミティブを 使用して構造化されたコードは タスク間の依存関係のチェーンを ランタイムに明確に理解させます これまで皆さんはSwiftの言語機能がどのように タスクが待機中に停止させることが できるかを学びました 実行中のスレッドは タスクの依存関係について推論し 別のタスクを選択することができます これは Swift 並行処理で記述されたコードが スレッドが常に前進できる実行時の取決めを 維持できることを意味します この実行時の取決めを利用して Swift 並行処理のための統合OSサポートを構築します これはSwift並行処理をデフォルトの エクセキュータとして支援するための 新しい協調スレッドプール形式になっています この新しいスレッドプールでは CPU コアと同じ数のスレッドのみを 生成するため システムが過剰に 動作しないことを保証をします 作業項目がブロックされると さらに多くのスレッドを生成する GCD の並行キューと異なり Swift スレッドは 必ず実行を進めることができます デフォルトのランタイムが 生成するスレッドの数を賢く制御します これにより 過度の同時実行の 既知の落とし穴を回避しながら Appに必要な並行処理を提供します Grand Central Dispatchによる並行処理についての 前回の WWDCトークにおいて私たちは皆さんに Appを個別のサブシステムに構造化して サブシステムごとに1つの シリアルディスパッチキューを使って 同時実行を制御することを推奨しました これだとスレッド爆増のリスクを冒さずに サブシステム内の並行処理を 1より大きくすることが 困難であることを意味しました Swiftを使用すると この言語はランタイムがを活用した 強力な不変条件を提供するため デフォルトのランタイムで より適切に制御された並行処理を 透過的に提供できます 皆さんはSwift並行処理のスレッドモデルについて さらに多く理解したので 皆さんのコードで これらのわくわくする新規機能を採用する際に 留意する事項についてお話ししましょう 留意する必要がある一つ目の事項は 同期コードを非同期コードに変換する際の パフォーマンスについてです 先ほど私達は 追加メモリーの 配置 及びSwift ランタイムなどの並行処理に関係する コストのお話をしました コードに並行処理を導入するメリットが コードを管理するコストを上回っている場合にのみ Swiftの並行処理を使用した新しいコードを 作成するよう注意する必要があります こちらのコードは ユーザーのデフォルトから 値を読み取るためだけに子タスクを生成しており 並行処理の追加の恩恵を 受けられない可能性があります これは子タスクにより行われる役立つ作業が タスクを作成し管理するコストにより 減少したためです Swiftの並行処理を採用する際の パフォーマンス特性を理解するために Instrumentsのシステムトレースを使用して プロファイリングすることをお勧めします 気をつける二つ目のことは await に関するアトミック性の観念です Swift は await 前にコードを実行したスレッドが continuation を取得するスレッドと 同じであることを保証しません 実際 await はコード内の明示的なポイントであり タスクは自身でスケジュールを解除できるため アトミック性が壊れていることを示します そのため待機中にロックを保持しないように 注意する必要があります 同様にスレッド固有のデータは awaitを挟んで保存されません スレッドの局所性を期待するコード内の仮定は await の一時停止動作を考慮して 再検討する必要があります そして最後に考慮することは Swiftの効率的なスレッドモデルの基礎となる ランタイムコントラクトに関係しています Swiftを使用すると この言語により スレッドが常に前進できるという実行時の取決めを 維持できることを思い出してください この取決めに基づいて Swiftのデフォルトのエグゼキュータとなる 協調スレッドプールを構築しました Swiftの並行処理を採用するときは 協調スレッドプールが最適に機能できるように コードでもこの取決めを 維持し続けることが重要です コード内の依存関係を明示的かつ既知にする 安全なプリミティブを使用することにより 協調スレッドプール内でこの取決めを 維持することができます await・アクター・タスクグループなどの Swift並行処理プリミティブを使用すると これらの依存関係はコンパイル時に判明します その為 Swift コンパイラはこれを強制し 実行時の取決めを維持するよう促します os_unfair_locks や NSLocks などのプリミティブも 安全ですが それらを使用する時は注意が必要です 同期コードでロックを使用することは タイトなクリティカルセクションで データ同期に使用する場合に安全です これは ロックを保持しているスレッドが ロックの解除に向けて 常に実行できるためです その時はプリミティブは競合下で スレッドを短期間ブロックする可能性がありますが フォワードプログレスの 実行時の取決めに違反しません Swift並行処理プリミティブとは異なり ロックの正しい使用方法を支援する コンパイラーのサポートはないため このプリミティブを正しく使用するのは ユーザーの責任です 一方 セマフォや条件変数などのプリミティブを Swift 並行処理で使用するのは 安全ではありません それらはコードの実行に依存関係を作りますが Swift ランタイムから 依存性情報は隠されてしまいます ランタイムはこの依存関係を認識しないため 正しいスケジューリング決定を行って それらを解決することができません 特に 構造化されていないタスクを生成する セマフォまたは安全でないプリミティブを使って タスクの境界を越えた依存関係を 遡及的に発生させないで下さい そのようなコードパターンは 別のスレッドがセマフォのブロックを 解除できるようになるまで スレッドがセマフォに対して無期限に ブロックできることを意味します これは スレッドの順方向進行の 実行時の取決めに違反します あなたのコードで そのような安全でない プリミティブの利用を特定するため 次の環境変数によりAppをテストしてください これにより 変更されたデバッグランタイムで Appが実行され 順方向進行の不変条件が適用されます この環境変数は 次に示すように Xcode のプロジェクトスキームの Run > Arguments で設定できます この環境変数を設定してAppを実行中に 協調スレッドプールのスレッドが ハングしているように見える場合は 安全でないブロッキングプリミティブが 使用されていることを示しています さて スレッドモデルがSwiftの並行処理向けに どのように設計されているかを学んだので この新しい世界で状態を同期するために 利用できるプリミティブについて もう少し掘り下げてみましょう アクターに関するSwift並行処理の講演で アクターを使用して mutable を 同時アクセスから保護する方法を見てきました 言い換えると アクターは 強力な新しい同期プリミティブを提供します アクターは相互排斥を保証することを 思い出してください アクターは一度に最大1つの メソッド呼び出しを実行できます 相互排斥とは アクターの状態に 同時にアクセスしないことを意味し データの競合を防ぎます アクターと他の形式の相互排斥を 比較してみましょう シリアルキューに同期して いくつかの記事でデータベースを更新する 前の例を考えてみましょう キューがまだ実行されていない場合は 競合はないと言います この場合 呼び出し元のスレッドは コンテキストスイッチなしで キューの新規作業項目を実行するために 再使用されます 代わりに シリアルキューが すでに実行されている場合 キューは競合しているということです この状況では 呼び出し元のスレッドは ブロックされます 最初の方でRokhiniが説明した通り このブロック行為は スレッド爆憎を引き起こすものです ロックにはこの同じ作用があります ブロックに関係する問題のため dispatch async を使用することを お勧めします dispatch async の主な利点は ブロックしないことです そのためたとえ競合があっても スレッド爆増を起こしません シリアルキューで dispatch async を 使用する欠点は 競合がない場合 呼び出し元のスレッドが他のことを続けている間 同時に非同期で作業を行なうには 新規スレッドをリクエストする必要があります この故に dispatch async の頻繁な使用は 過度のスレッド生成とコンテキストスイッチを 引き起こす可能性があります そこでアクターを使います Swiftのアクターは 効率的なスケジューリングのために 協調スレッドプールを利用することにより 両方の世界の一番良い部分を組み合わせます 実行中でないアクターでメソッドを呼び出すと 呼び出し元のスレッドを再利用して メソッド呼び出しを実行します 呼び出されたアクターがすでに実行中の場合 呼び出し元のスレッドは実行中の関数を一時停止し 他の作業を選択できます これら二つのプロパティが例のニュースAppで どのように機能するかを見てみましょう データベースとネットワークサブシステムに 焦点を当てましょう Swift 並行処理を利用するためにAppを更新する際 データベース用のシリアルキューは データベースアクターに置き換えます ネットワーク用の並行処理キューは 各ニュースフィードごとに 一つのアクターと置き換えます 単純化のため 三つのフィードアクターのみを表示しました スポーツフィード お天気フィード そして健康フィード ただし実際はさらに多くのものがあります これらのアクターは 協調スレッドプールで実行されます フィードアクター はデータベースにアクセスして 記事を保存し その他のアクションを行います これにはあるアクターから他のアクターへの 実行の切り替えが含まれます この手順を「アクターホッピング」と呼びます アクターホッピングが どのように 機能するかをお話ししましょう スポーツフィードのアクターが 協調プールのスレッドで実行されており いくつかの記事をデータベースに保存することを 決定したとします 現在データベースが使用されていないと考えます これは競合のない場合です スレッドはスポーツフィードファクターから データベースアクターに直接ホップします ここで気づくことが二つあります まず アクターをホッピングしている間 スレッドはブロックされませんでした 第二に ホッピングは別のスレッドを 必要としませんでした ランタイムは スポーツフィードアクターの ワークアイテムを直接一時停止させ データベースアクターの 新しい作業項目を作成できます データベースアクターがしばらく実行されますが 最初の作業項目が完了していないとします この時天気フィードアクターが データベースに いくつかの記事を保存しようとしていると 仮定します これにより データベースアクターの 新しい作業項目が作成されます アクターは 相互排斥を保証することによって 安全性を確保します 一度にアクティブにできる作業項目は 最大で1つです アクティブな作業項目D1がすでに1つあるため 新しい作業項目D2は保留されたままになります アクターもノンブロッキングです この状況では 天気フィードアクターは一時停止され 実行されていたスレッドは 他の作業を行うために開放されます 少し経過した後 当初のデータベースリクエストが完了すると データベースアクターのアクティブな 作業項目は削除されます この時点でランタイムは データベースアクターの保留中の作業項目の 実行を開始することを選択できます またはフィードアクターの一つを 再開することも選べます または 解放されたスレッドで その他の作業を行うこともできます 多くの非同期作業があるとき 特に多くの競合があると システムはどの作業が最も重要かに基づいて トレードオフを行う必要があります 理想的にはユーザーインタラクションを含むなど 優先順位の高い作業が バックアップの保存などの裏方の作業に対して 優先権を持ちます アクターはリエントラントの概念により システムが作業の優先順位をうまく つけられるように設計されています ここでリエントラントがなぜ重要なのかを 理解するため まず GCD の優先権を処理する方法を 見てみましょう シリアルデータベースキューを備えた 先程のニュースAppについて考えてみます データベースが UI を更新するための 最新データを取ってくるなど 高優先順位の作業を受け取るとします またデータベースをiCloudにバックアップするなど 優先度の低い作業も受けます これはある時点で行う必要がありますが 必ずしもすぐに行う必要はありません コードが実行されると 新しい作業項目が作成され インターリーブされた順序で データベースキューに追加されます ディスパッチキューは受信した項目を 厳格な先着順で実行します あいにくこれは項目Aが実行された後 次の高優先項目に達する前に 5つの低優先順位の項目が 実行される必要があります これは「優先順位の逆転」と呼ばれます シリアルキューは キューの中の 高い優先順位の作業の前にある 全ての作業の優先順位を上げることにより 優先順位の逆転を回避します 実際には これはキュー内の作業が より早く行われることを意味します ただし アイテムBの実行を開始する前に アイテム1から5を完了する必要があるという 主要問題を解決しません この問題の解決には厳格な先着順から離れて セマンティックモデルを変更する必要があります アクターのリエントラントです 再入可能性が順序決定にどのように 繋がるかの例を挙げて見てみましょう
スレッドで実行している データベースアクターを考えましょう それは停止して いくつかの作業を待っており スポーツフィードアクターがそのスレッドで 実行を開始するとします 少し経過した後 スポーツフィードアクターが記事を保存するため データベースアクターを呼び出します データベースアクターは競合していないため 保留中の作業項目があっても スレッドはデータベースアクターにホップできます 保存オペレーションを行うために新規作業項目が データベースアクター用に作成されます これがアクター再入可能性が意味することです アクターの新規作業項目は 他の古い作業項目が停止されている間に 実行される可能性があります アクターは依然相互排斥を維持し 特定の時間に実行できる作業項目は最大で1つです しばらくするとアイテムD2の実行が終了します D1の後に作成されたにもかかわらず D2がD1の前に実行を終了したことに 注意してください したがって アクターの再入可能性のサポートは アクターが厳密に先入れ先出しではない順序で アイテムを実行できることを意味します 以前の例をもう一度見てみましょう ただし シリアルキューの代わりに データベースアクターを使用します まず優先度が高い作業項目Aを実行します それが完了すると 以前と同じ優先順位の逆転があります アクターは再入可能性を考慮して 設計されているため ランタイムは 優先度の低いアイテムよりも 優先度の高いアイテムを キューの先頭に移動することを選択できます このようにして優先度の高い作業を最初に実行し 優先度の低い作業を後で実行することができます これにより 優先順位の逆転の問題に直接対処し より効果的なスケジューリングと リソースの利用が可能になります 協調プールを使用するアクターが 相互排斥を維持し作業の効果的な優先順位付けを サポートするように設計されていることについて 少しお話ししました 別の種類のアクターである メインアクターの特性は 既存のシステムの観念である メインスレッドを抽象化するため幾分異なります アクターを使用するニュースAppの 事例を考えてみましょう ユーザーインターフェースを更新するには MainActorに依頼する必要があります メインスレッドは協調プールのスレッドから 切り離されている為 これにはコンテキストスイッチが必要です コード例を使用して このパフォーマンスの影響を見てみましょう 次のコードを考えてください MainActor の updateArticles 関数があります 記事をデータベースから読み込み 各記事の UI を更新します ループの各反復には 少なくとも2つのコンテキストスイッチが必要です 一つはメインアクターから データベースアクターへホップ もう一つはポップして戻ります このようなループの CPU 使用率を見てみましょう 各ループの反復には 2つのコンテキストスイッチが必要なため 2つのスレッドが短時間次々に実行される 繰り返しパターンがあります ープの反復回数が少なく 各反復でかなりの作業が行われている場合は おそらく問題ありません しかしメインアクターを頻繁にオン オフする場合 スレッド切り替えのオーバーヘッドが 増加し始める可能性があります コンテクストスイッチングに 多くの時間を費やしている場合は メインアクターの作業がバッチ処理されるように コードを再構築する必要があります ループを loadArticles および updateUI メソッドにまとめて 一度に1つの値ではなく 配列を処理するようにすることで バッチ作業を行うことができます 作業をまとめることでコンテスト スイッチの回数が減少します 協調プール上のアクター間のホッピングは 高速ですが メインアクターとの間のホップに 注意する必要があります 振り返ってみると 協調スレッドプールの設計 ノンブロッキングサスペンションのメカニズム アクターの実装方法まで システムを最も効率的にするために どのように取り組んだかを 学びました 各ステップで ランタイムコントラクトを使用して Appの性能を向上させています これらの信じられないほどの 新しい言語機能を使用して 明確で効率的で楽しいSwiftコードを 作成する方法を見て興奮しています ご視聴ありがとうございました WWDC をお楽しみください ♪
-
-
4:57 - GCD code with hidden performance pitfalls
func deserializeArticles(from data: Data) throws -> [Article] { /* ... */ } func updateDatabase(with articles: [Article], for feed: Feed) { /* ... */ } let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: concurrentQueue) for feed in feedsToUpdate { let dataTask = urlSession.dataTask(with: feed.url) { data, response, error in // ... guard let data = data else { return } do { let articles = try deserializeArticles(from: data) databaseQueue.sync { updateDatabase(with: articles, for: feed) } } catch { /* ... */ } } dataTask.resume() }
-
13:18 - Swift concurrency equivalent using a task group
func deserializeArticles(from data: Data) throws -> [Article] { /* ... */ } func updateDatabase(with articles: [Article], for feed: Feed) async { /* ... */ } await withThrowingTaskGroup(of: [Article].self) { group in for feed in feedsToUpdate { group.async { let (data, response) = try await URLSession.shared.data(from: feed.url) // ... let articles = try deserializeArticles(from: data) await updateDatabase(with: articles, for: feed) return articles } } }
-
15:16 - Async functions: stack frames and async frames
// on Database func save(_ newArticles: [Article], for feed: Feed) async throws -> [ID] { /* ... */ } // on Feed func add(_ newArticles: [Article]) async throws { let ids = try await database.save(newArticles, for: self) for (id, article) in zip(ids, newArticles) { articles[id] = article } } func updateDatabase(with articles: [Article], for feed: Feed) async throws { // skip old articles ... try await feed.add(articles) }
-
37:13 - Excessive context switching due to Main actor hoppping
// on database actor func loadArticle(with id: ID) async throws -> Article { /* ... */ } @MainActor func updateUI(for article: Article) async { /* ... */ } @MainActor func updateArticles(for ids: [ID]) async throws { for id in ids { let article = try await database.loadArticle(with: id) await updateUI(for: article) } }
-
38:18 - Batch UI work to reduce the number of context switches
// on database actor func loadArticles(with ids: [ID]) async throws -> [Article] @MainActor func updateUI(for articles: [Article]) async @MainActor func updateArticles(for ids: [ID]) async throws { let articles = try await database.loadArticles(with: ids) await updateUI(for: articles) }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。