ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swiftの並行処理を視覚化して最適化する
InstrumentsのSwift Concurrencyテンプレートで、Appを最適化する方法をご覧ください。パフォーマンスに関する一般的な問題について解説し、Instrumentsを使用してこれらの問題を見つけて解決する方法を紹介します。さらに、App内でUIの応答性を維持したり、並列パフォーマンスを最大化したり、Swiftの並行処理アクティビティを分析したりする方法について解説します。 このセッションを最大限に活用するには、Swiftの並行処理(タスクとアクタなど)に関する知識を習得しておくとよいでしょう。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
-
ダウンロード
♪ ♪
Swift並行処理を視覚化して 最適化するへ ようこそ Swift runtime libraryで働いている Mikeです HarjasですInstrumentに取り組んでいます Swift 並行処理コードをよりよく理解し すばやく実行する方法について説明します Instrument 14で使える 新しいツールも含まれます まずはSwift 並行処理の さまざまな部分の連携を 軽く復習してスピードを上げましょう 新しい並行処理 instrument のデモをその後に行います Swift 並行処理のAppを使い実際の パフォーマンス問題を 解決する方法を説明します スレッドプールの枯渇と誤用の連続という 潜在的な問題を防ぐ方法 について最後に説明します Swift 並行処理は昨年 発表されました これは 非同期/待機の構造化並行処理で アクターを含む新しい言語機能です それ以来 Appleの内外で これらの機能が多く採用され 嬉しく思っています Swift 並行処理は同時プログラミングが より簡単かつ安全になるよう 新機能を追加しました 非同期/待機は並行コードの 基本的構文の構成要素です これで 実行中の作業を中断し 実行スレッドをブロックせず 後でその作業を再開できる関数を作成して 呼び出すことができます
タスクは並行コードで基本的な作業単位で 並行コードを実行し状態と 関連データを管理します ローカル変数キャンセルの処理 非同期コードの実行の開始と中断を含みます 構造化された並行処理で 子タスクを並行して生成し 完了まで待つことが簡単になります これは作業をグループ化し 使用しない場合にタスクが 待機や自動的に中止される構文が提供されます アクターはアクセスの必要な 複数のタスクを調整します データを外部から切り離し 内部で一度に1つのタスクだけ 操作できるようにし 同時変更によるデータ競合を回避します Instruments 14の新機能では App内でアクティビティを すべて視覚化できる一連の instrumentを導入しています これでAppの動作を把握し問題を特定し パフォーマンスを向上させることができます Swift 並行処理の基本の詳細は 関連ビデオセクションにリンクされている ビデオをご覧ください
Swift 並行処理コードを使って App最適化方法を見ます Swift並行処理で正しい並行と並列コードを 簡単に記述できます 並行処理構造を誤使用する コードの記述も可能です 正しく使うこともできますが 期待するパフォーマンス上の 利点は得られません
Swift並行処理でコードを 記述すると一般的問題として パフォーマンス低下や バグ発生の可能性があります 主アクターブロックで フリーズの可能性があります アクターの競合とスレッドプールの枯渇は 並列実行を減らすことで パフォーマンスが低下します 継続的な誤用は漏洩や クラッシュを引き起こします Swift 並行処理 instrument は発見と解決に役立ちます 主アクターブロックから それぞれを見ていきましょう 主アクターブロックは長時間実行中のタスクが 主アクターで実行されたときに発生します これはメインスレッドでの すべての作業を実行する 特別なアクターです UIの作業はメインスレッドで行う必要があり 主アクターでUIコードを Swift 並行処理に統合可能です しかし メインスレッドはUIに 非常に重要なため使用可能な 必要があり長時間稼働する 作業単位で占有はできません この場合 ハングアップし Appは応答しなくなります 主アクターで実行されるコードはすぐに終了し 作業を完了するか計算を主アクターから バックグラウンドに移動する必要があります 作業は通常のアクターか 独立タスクに配置することで バックグラウンドに移動できます 主アクターで小作業単位を 実行してUIを更新したり メインスレッドで必要な 他のタスクを実行できます このデモを見てみましょう ありがとう Mike ここではSqueezerAppを使用しています このAppはフォルダ内の すべてのファイルをすばやく 圧縮できるように構築されています 小さなファイルはうまく動いているようですが サイズの大きいファイルを 使う場合は予想よりも 時間がかかりUIが完全に フリーズし応答しません これはユーザーに非常に影響を与え Appがクラッシュしたか終了しないと考える 可能性があります UIが常に応答しユーザーに 最高の体験を提供できるよう 努力する必要があります このパフォーマンスの問題を調査するには Instrumentsの新しいテンプレートを使います SwiftタスクとアクターのInstrumentsには 並行処理を視覚化し最適化 するツールが揃っています パフォーマンスの問題を調査し始めたら Swift タスク instrumentが提供する トップレベルの統計を見てみましょう 1つ目は実行タスクで これは同時に実行されている タスクの数を示します 次に特定の時点でのタスクの数を示す アライブタスクです 最後にタスクの合計です その時点までに作成されたタスクの合計数が グラフで表示されます Appメモリの占有量を減らす場合は アライブと合計タスクの 統計情報を確認してください これらの統計をすべて組み合わせることで コードが並列化されているかどうかと 使用しているリソースの数を把握できます この詳細ビューの1つはタスクフォレストです このウィンドウの下半分には構造化された 並行処理コード内のタスク間の親子関係が 図式的に表示されます 次にタスクの概要ビューが表示されます 各タスクが異なる状態で 費やした時間を示しています タスクを右クリックすると 選択したタスクに関する すべての情報を含むトラックをタイムラインに すべての情報を含むトラックをタイムラインに これにより非常に長い間 実行されているタスクや アクターへのアクセスを待機しているタスクを すばやく検索して知ることができます Swiftタスクをタイムラインに固定すると 主な4つの機能が得られます まず状態を示すトラックです 2つ目は拡張詳細ビューのバックトレースです 3つ目はSwiftタスクの状態に関する詳細な コンテキストを提供する説明ビューです たとえばタスクが待機している場合 それが通知されます 最後はサマリビューで説明ビューと同じ アクションピンにアクセスできます これで 子タスク スレッド Swiftアクターをタイムラインに固定できます 説明ビューはSwiftタスクが 他の並行処理プリミティブと CPUに関連しているか調べるのに役立ちます これで新しいinstrumentの機能の概要を 簡単に確認できたのでAppのプロファイルを 簡単に確認できたのでAppのプロファイルを XcodeでCommand-Iを押し プロジェクトを引き出します Appがコンパイルされinstrumentが開き 目的のSqueezerAppが事前に選択されます テンプレートピッカーの Swift 並行処理を選択して 録音を開始できます
もう一度大きなファイルをAppにドロップします
またAppが回転し始めUIが応答しません これを数秒間実行して InstrumentsがAppに関する 全情報を取得可能にします
出力が表示されたら調査を開始できます 全情報を見やすくするため フルスクリーン表示にします
オプションキーを押しながら ドラッグすると拡大できます
追跡過程で UIハングが発生している場所が表示されます これはハングがいつ発生し どのくらい時間が経過したか 明確でない場合に便利です すでに述べたようにトップレベルの Swift タスクの統計情報を 確認するには良い場所です すぐにわかるのが実行中のタスク数です ほとんどは1つのタスクだけが実行されています この問題の一部はすべての仕事が直列化して 強制されていることを私たちに教えています タスクの状態サマリーを使い 最長の実行タスクを調べ アクションピンを使用して タイムラインに固定できます
このタスクの説明ビューは バックグラウンドスレッドで 短時間実行された後 メインスレッドで長時間実行 されたことを示しています 詳細を調べるにはタイムラインに固定します
メインスレッドは複数の タスクにブロックされます Mikeが話した主アクターの ブロックの問題です 自分自身に尋ねる必要がある質問は このタスクは何をしてどこから来たのか? 説明ビューに切り替えて 質問に答えることができます 拡張詳細ビューのバックトレースはタスクが compressAllFiles関数で 作成されたことを示します この説明はタスクがcompressAllFilesで 1番の閉鎖を実行していることを示しています これを右クリックすると ソースビューアで開けます
この関数内の1番の閉鎖は 圧縮コードを呼び出します このタスクが作成場所と 実行内容がわかったので Xcodeでコードを開きメインスレッドでこれらの 重い計算を実行しないように 調整できます CompressionStateクラス内に compressAllFiles関数はあり 全体に注釈が付き主アクターで実行されます タスクがメインスレッドでも 実行された理由を説明します このクラスは主アクターにある必要があります publishedプロパティはメインスレッドからのみ 更新しないと実行時に問題が 発生する可能性があります 代わりにこのクラスを独自の アクターに変換もできます しかしコンパイラは基本的に この共有可変状態を 2つの異なるアクターによって 保護する必要があるので これを行うことができないと言います 実際の解決策のヒントにはなりません このクラスには2つの異なる可変状態があります このクラスには2つの異なる可変状態があります 主アクターと分離する必要があります 他の状態ログへのアクセスは同時アクセスから 保護する必要がありますが スレッドがどの時点で アクセスログするかは重要ではありません よって実際は主アクター上に ある必要はありません ただし同時アクセスから保護したいので 独自のアクターにまとめます 必要なのはタスクを2つの間を 動き回る方法の追加です ParallelCompressorという 新しいアクターを作りました
ログ状態を新しいアクターにコピーし 特別なセットアップコードを追加できます
ここからアクターが互いに 通信できる必要があります まず ログ変数を参照するコードを CompressionStateクラスから削除し ParallelCompressorアクターに追加します
最後にParallelCompressorで compressFileを呼び出し CompressionStateを更新する必要があります
これらの変更を適用して再びAppをテストします もう一度 大きなファイルをAppにドロップします
UIはもうハングしません これは大きな改善ですが 期待したスピードではありません 機械のすべてのコアを最大限に活用して この作業を可能な限り速く したいと考えています Mike 他に何を見るべきでしょうか?
主アクターの作業を中断して ハングを解決しましたが 期待したパフォーマンスを得られていません 確認するにはアクターを 詳しく見る必要があります アクターを使い複数タスクで 共有状態で操作ができますが ユーザーがそこへアクセスを 連携すると処理が実行します アクターを占有できるタスクは一度に1つだけで それを使う必要がある他のタスクは待機します Swift並行処理で非構造化 タスク タスクグループ 非同期レットを使用した 並列計算が可能になります 理想的に構成要素は多数の CPUコアを同時に使えます このようなコードアクターを 使いタスクで共有されます アクターでの多くの作業の 実行には注意してください 複数タスクが同じアクターを 同時に使用しようとすると アクターはそれらのタスクの実行を連携し 並列計算のパフォーマンス上利点は失われます
各タスクはアクターが使用 できるまで待機が必要です アクターのデータへ排他的 アクセスが必要な場合のみ タスクが実行されるようにする必要があります 他は全てアクターから 実行される必要があります タスクをチャンクに分けます アクターで実行と実行しない チャンクがあります 非アクターの分離チャンクは 並行して実行できるため コンピュータはより速く作業を完了できます この動作のデモを見てみましょう ありがとう Mike 「File Squeezer」Appの更新を見てみましょう Mikeが教えてくれたことを 覚えていてください タスク概要には並行処理コードが待機状態で 警告時間を費やしていることが示されています アクターへ多くのタスクが 待機してることを意味します これらのタスクの1つを固定し その理由を学習しましょう
圧縮作業の実行前のタスクは かなりの時間を要して ParallelCompressorアクターに到達します アクターをタイムラインに固定しましょう
ParallelCompressorアクター のトップレベルデータです 長時間実行中のタスクでブロックされています タスクは必要な間アクターに留まる必要があり タスクの説明に戻りましょう
ParallelCompressorをキューに加えた後 タスクはcompressAllFilesの クローズ番号1で実行されます では 調査を始めましょう ソースコードはクローズの 圧縮作業を示しています ParallelCompressorアクターの一部である compressFile関数がアクターで実行されると 他のすべての圧縮作業がブロックされます これを解決するには関数を Acter-isolationから取り出し 独立タスクに移動する必要があります
これで関連する可変状態を更新するため 必要な限りアクター上で 独立タスクを実行できます 圧縮関数はActor-protected領域にアクセスする 必要があるまでスレッドプール内の 任意のスレッドで自由に実行できます たとえば「files」プロパティ にアクセスが必要な場合は 主アクターに移動しますが 作業が完了するとすぐ再び 「並行処理の海」に移動し ログプロパティにアクセスが必要で このためParallelCompressor アクターに移動します しかし ここでも完了すると 直ぐにスレッドプールで実行 するアクターを再び残します 圧縮作業を行うタスクは1つだけではなく 多くのあります アクターに抑制されてない ためスレッドの数だけ 同時に実行できますが
各アクターは一度に1つの タスクしか実行できません ほとんどタスクはアクターに ある必要はありません Mikeが説明したように 圧縮タスクを並行して実行し 使用可能なCPUコアを許可できます では 変更してみましょう
compressFile関数を 非絶縁としてマークできます
これでコンパイラエラーがいくつか発生します 非絶縁としマークすることで このアクターの共有状態に アクセスの必要がないことを Swiftコンパイラに伝えます しかし必ずしもそうではありません ログ関数 Actor-isolatedは 可変状態へ接続が必要です これを修正するにはこの関数をasyncにしてから awaitキーワードでログ起動マークが必要です
次にタスクの作成を更新して 独立タスクの作成が必要です
これは作成されたActor-contextを 引き継がないようにタスクを守るためです 独立タスクのために 自己保存する必要があります
もう一度Appをテストしてみましょう
Appはすべてのファイルを同時に圧縮でき UIは応答し続けます Swift Actors instrumentを 確認し改善点を検証します ParallelCompressorアクターを見ると アクターで実行される作業のほとんどは 短時間で 待ち時間はほとんどありません 要約するとInstrumentを使い UIがハングする原因を特定し よりよい並列処理のために 並行処理コードを再構成し パフォーマンスの改善を データを使って検証しました Mikeがパフォーマンスの 他の潜在的問題を説明します デモで見たこと以外に よくある2つの問題があります まずスレッドプールの枯渇を説明します これでAppのパフォーマンス低下や さらにデッドロックになる可能性があります Swift並行処理には実行中に 成長するタスクが必要です タスクが何かで待機すると通常は一時停止します ただしタスク内のコードでは ファイルやネットワークI/O ロックの取得など呼び出しを 中断せずに実行できます これで タスクを進めていく 必要条件ではなくなります この時 タスクは実行中の スレッドを占有し続けますが 実際はCPUコアを使っていません スレッドプールは制限され 一部はブロックされるため 並行処理ランタイムは全部の CPUコアを完全に使えません これにより実行できる並列計算の量と Appのパフォーマンスが最大化されます 極端にはスレッドプール全体 がブロックタスクに占領され 新しいタスク実行が待機している場合 並行処理ランタイムがデッド ロックすることがあります タスクのコールをブロック しないようにしてください ファイルとネットワークIOは 非同期APIで行います 条件変数やセマフォの状態を 待たないでください 必要であれば細かい設定や短時間ロックは 許可されますが競合が多いロックや 長期間のロックは避けてください これらの操作が必要なコードがある場合は ディスパッチキューで実行するなど 並行処理スレッドプールの外に移動し 連続を使用して並行処理世界に橋渡しします できれば 非同期APIを使って操作をブロックし システムの円滑な動作を維持します 継続を使う場合は正しく使う注意が必要です Swift並行処理とその他形式の 非同期コード間の橋渡しです 継続は現在のタスクを中断し コールバックを提供し 呼び出し時にタスクを再開します これはコールバックベースの 非同期APIで使えます Swift並行処理の観点からタスクは中断され 継続が再開されると再開されます コールバックベースの非同期APIの観点からは 作業が開始し完了すると コールバックが呼ばれます Swift 並行処理 instrumentは継続を認識し それに応じて時間間隔を評価します これはタスクの継続を 待っていることを示します 継続コールバックには特別な要件があり 1回だけでそれ以上コールする必要はありません コールバックベースのAPIでは 一般的な要件ですが 非公式になる傾向があり 言語によって強制されることはなく 管理は共通です Swift 並行処理には確かに必要な要件です Swift 並行処理には確かに必要な要件です コールバックがないとタスクは漏出します このコードスニペットでは継続チェックを使い 継続を取得します コールバックベースのAPIを次に起動します コールバックでは継続を再開します これは1回だけ呼び出すという 要件を満たしています コードが複雑な場合は注意が必要です 左側では成功した場合のみ 継続を再開するように コールバックを変更しました コールバックを変更しました 失敗した場合継続は再開されず タスクは永久に停止されます 右側では継続を2回再開しています これもバグでAppが不正処理や停止したりします これらのスニペットは 継続を1回だけ再開するという 要件に違反しています チェックと危険の継続の2種類を使用できます 動作が極めて重要でない限り継続には常に 継続チェックAPIを使ってください 継続チェックは自動的に 誤作動を検出し印を付けます 継続チェックが2回呼び出されると 継続トラップが発生し 継続が 呼び出されず破棄されると 漏出したことを示すメッセージが コンソールに表示されます Swift 並行処理 instrumentは 対応するタスクが継続状態で 無期限にスタックしたことを示します Instrumentsには新しいSwift 並行処理 テンプレートがたくさんあります 構造化された並行処理のグラフィックの可視化 タスク作成の表示正確な組立説明の検査を行い Swift 並行処理ランタイムの 全容を把握できます Swift 並行処理がフードの下で どう機能するかは昨年の 「Swiftの並行処理:舞台裏」をご覧ください 「Swiftの並行処理でデータ競合を排除する」 ではデータ競合の詳細を確認してください ご視聴ありがとうございました! 並行処理コードのデバッグ作業も楽しいです
-
-
10:24 - CompressionState class
@MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var logs: [String] = [] func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task { let compressedData = compressFile(url: file.url) await save(compressedData, to: file.url) } } } func compressFile(url: URL) -> Data { log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in update(url: url, uncompressedSize: uncompressedSize) } progressNotification: { progress in update(url: url, progress: progress) log(update: "Progress for \(url): \(progress)") } finalNotificaton: { compressedSize in update(url: url, compressedSize: compressedSize) } log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) }
-
11:49 - CompressionState class using ParallelCompressor actor
actor ParallelCompressor { var logs: [String] = [] unowned let status: CompressionState init(status: CompressionState) { self.status = status } func compressFile(url: URL) -> Data { log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in Task { @MainActor in status.update(url: url, uncompressedSize: uncompressedSize) } } progressNotification: { progress in Task { @MainActor in status.update(url: url, progress: progress) await log(update: "Progress for \(url): \(progress)") } } finalNotificaton: { compressedSize in Task { @MainActor in status.update(url: url, compressedSize: compressedSize) } } log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) } } @MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var compressor: ParallelCompressor! init() { self.compressor = ParallelCompressor(status: self) } func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task { let compressedData = await compressor.compressFile(url: file.url) await save(compressedData, to: file.url) } } } }
-
17:46 - CompressionState class using ParallelCompressor with minimal actor-isolation and detached tasks
actor ParallelCompressor { var logs: [String] = [] unowned let status: CompressionState init(status: CompressionState) { self.status = status } nonisolated func compressFile(url: URL) async -> Data { await log(update: "Starting for \(url)") let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in Task { @MainActor in status.update(url: url, uncompressedSize: uncompressedSize) } } progressNotification: { progress in Task { @MainActor in status.update(url: url, progress: progress) await log(update: "Progress for \(url): \(progress)") } } finalNotificaton: { compressedSize in Task { @MainActor in status.update(url: url, compressedSize: compressedSize) } } await log(update: "Ending for \(url)") return compressedData } func log(update: String) { logs.append(update) } } @MainActor class CompressionState: ObservableObject { @Published var files: [FileStatus] = [] var compressor: ParallelCompressor! init() { self.compressor = ParallelCompressor(status: self) } func update(url: URL, progress: Double) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].progress = progress } } func update(url: URL, uncompressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].uncompressedSize = uncompressedSize } } func update(url: URL, compressedSize: Int) { if let loc = files.firstIndex(where: {$0.url == url}) { files[loc].compressedSize = compressedSize } } func compressAllFiles() { for file in files { Task.detached { let compressedData = await self.compressor.compressFile(url: file.url) await save(compressedData, to: file.url) } } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。