1. シナリオと制約
あなたは画像編集アプリを保守しています。ネットワーク取得、デコード、サムネイル生成に高い負荷がかかる一方で、UIはキャッシュにアクセスします。古いコードは暗黙的なスレッドホップに依存しており、一部のAPIはnonisolated asyncであり、テストは最終的な画像のみを検証しています。Swift 6.2を導入し、スムーズなスクロールと決定論的な結果を維持しながら、Swift 6のデータ競合チェックを段階的に有効にする必要があります。
まずベースラインの記録から始めます。移行前後のメインスレッドのフレーム時間、デコードスループット、アクティブなタスク数、キャッシュヒット率、キャンセルレイテンシ、コンパイラ診断を測定します。これらがないと、パフォーマンスの低下と正確性の修正を混同しやすくなります。
2. 最初にSwift 6.2の変更点を説明する
Swift 6.2は、ターゲット内のコードをデフォルトでメインアクターに分離するオプション設定-default-isolation MainActorを提供します。また、並行して実行することを意図した関数をマークするための@concurrentも追加されています。NonisolatedNonsendingByDefaultを使用すると、nonisolated async関数はグローバルな並行エグゼキュータに自動的に移動するのではなく、デフォルトで呼び出し元のアクターを維持します。並行セマンティクスが意図されている場合は@concurrentを使用します。
これらのオプションはアノテーションのノイズを減らしますが、すべての操作を並列にするわけではありません。デフォルトの分離は、誰が状態に安全にアクセスできるかを記述します。@concurrentは、処理が現在のアクターを離れて並列に実行される可能性があることを明示的に示します。
3. 分離境界を設定する
ミュータブルなキャッシュとUIの状態は@MainActor型に保持します。不変の入力と出力を持つ純粋な値変換およびデコーダはSendableにします。ダウンロード層は、隠れたスレッド親和性を持つオブジェクトではなく値を返す必要があります。すべてのAPIについて、呼び出し元のアクター、保護された状態、および並列実行が許可されているかどうかを文書化します。
例えば、キャッシュの保護はメインアクターに維持します。
@MainActor
final class ImageCache {
private var values: [URL: Image] = [:]
func value(for url: URL) -> Image? { values[url] }
func insert(_ image: Image, for url: URL) { values[url] = image }
}警告を消すためだけに、古いコードのあちこちにMainActor.runを散らばらせてはいけません。移行ガイダンスではこれを暫定的なツールとして扱っており、長期的なAPIはその型やシグネチャで分離を表現すべきです。
4. @concurrentを使用するタイミングを判断する
処理を並列に実行しても安全であり、その入出力がsending要件を満たし、ベースラインで効果が確認できる場合にのみ@concurrentを追加します。画像デコードは多くの場合これに該当します。メインアクターのキャッシュを直接操作せず、新しく作成された値を返すためです。
@MainActor
struct ImagePipeline {
static func image(from url: URL) async throws -> Image {
let cached = cache.value(for: url)
if let cached { return cached }
let image = try await decode(url: url)
cache.insert(image, for: url)
return image
}
@concurrent
private static func decode(url: URL) async throws -> Image {
let (data, _) = try await URLSession.shared.data(from: url)
return try ImageDecoder.decode(data)
}
}@concurrentは速度向上のスイッチではないことを指摘してください。過剰な使用はスケジューリング、メモリ、同期のコストを増加させます。デコーダが内部的にシリアルである場合や画像が極小である場合、スループットが低下する可能性があります。
5. 呼び出し元コンテキストとSendableを処理する
呼び出し元コンテキストのセマンティクスが有効な場合、async関数は呼び出し元のアクター上で継続することがあります。これは呼び出し元に分離された状態を必要とする場合に役立ちますが、長いCPU処理はそのアクターをブロックする可能性があるため、純粋な@concurrentステージに切り離します。アクター間を渡る値はSendableであるか、アクター、ロック、または同等の同期境界によって保護されている必要があります。
すべての診断を@unchecked Sendableで黙らせないでください。不変条件、同期プリミティブ、ライフタイムを証明した後の限定的な最終手段としてのみ使用し、並行処理のストレステストと組み合わせてください。
6. ツールを用いて段階的に移行する
まず1つのモジュールで先行機能と厳格な診断を有効にし、移行ツールを活用して警告とfix-itに対処します。公開APIの分離とSendable制約を安定させ、その後そのモジュールをSwift 6言語モードに切り替えます。各フェーズはロールバック可能にしておき、モジュールごとに診断結果をアーカイブします。
実用的な順序は、値型とプロトコル制約、低レベルサービス、キャッシュアクター、そして最後にUI呼び出し元です。これにより、変更された何百ものawait式ではなく、境界部分にエラーが表示されるようになります。互換性作業中は、実装モジュール内のチェックレベルを引き上げつつ、既存のクライアント向けにSwift 5モードのインターフェースを維持します。
7. 競合と意図しないシリアライズの不在を証明する
機能テストにとどまらず、並行処理のストレスを追加します。単一URLに対する同時リクエスト、ランダムなキャンセル、繰り返される書き込み、アクター間の読み取りなどです。Thread Sanitizerと厳格な並行性診断を使用してデータ競合を検出します。コールドキャッシュとウォームキャッシュ、画像サイズ、並行数の制限をベンチマークし、フレーム時間、スループット、ピークメモリ、キャンセルレイテンシを比較します。
タスクコンテキストと非同期スタックを調査し、キャッシュアクセスがメインアクターに残ったままデコードが実際に並行して実行されていることを確認します。@concurrentがフレーム時間を改善しない場合は、ブロッキングするデコーダ、処理をシリアライズする単一のロック、または小さなタスクが多すぎないか確認してください。
8. ルーブリックとフォローアップ
説明が必須の項目
- デフォルトの分離、呼び出し元コンテキスト、明示的な並列処理を区別すること。
asyncはバックグラウンドスレッドを意味するものではない。 @unchecked Sendableでエラーを隠すのではなく、Sendable、アクター境界、キャンセルを用いて安全性を証明すること。- 段階的な移行、ストレステスト、パフォーマンスベースラインを、リグレッションシグナルの説明とともに提示すること。
フォローアップの質問
- デコーダがスレッドセーフでない場合、アクター、シリアルエグゼキュータ、別プロセスサービスのどれの背後に配置しますか?またその理由は何ですか?
- キャッシュ全体をシリアライズすることなく、単一URLに対する重複ダウンロードをどのように統合(coalesce)しますか?
- 移行中にSwift 5とSwift 6のモジュール間でAPIを共有するにはどうすればよいですか?また、どの制約を最初に出荷すべきですか?
採点ガイド
優れた回答は、分離モデリング、移行ツール、実行時のエビデンスを結びつけます。状態と実行コンテキストを定義し、並列処理のために必要最小限の@concurrentサーフェスを使用し、コンパイラ診断、Sanitizer、ベンチマークデータを用いて結果を検証します。