はじめに
Swift 6 では、安全な並行処理を推進するため、データ競合を防ぐ厳格なチェックが導入されました。
これにより安全性が向上した一方で、既存コードの多くでコンパイルエラーが発生する可能性が生じました。
この課題に対し、SE-0461 が提案され、Swift 6.2 から nonisolated(nonsending)
キーワードと、それをデフォルトで有効にするビルド設定 nonisolated(nonsending) By Default
が Xcode 26 で導入されます。
これらの機能により、async
関数がデフォルトで呼び出し元のアクターを継承するようになります。
本記事では、
- Swift 6 で厳格になった並行処理チェックとその影響
- Swift 6.2 の
nonisolated(nonsending)
による緩和策 - Xcode 26 のビルド設定
nonisolated(nonsending) By Default
と併用を検討すべき@concurrent
について紹介します。
Swift 6 での並行処理の厳格なチェック
Swift 6 ではデータ競合を防ぐため、並行処理の厳格なチェックが導入されました。
Sendable
プロトコルへの準拠やアクター隔離が厳密に検証されます。
次のコードはその例です。
struct ContentView: View {
let repository = Repository() // Repository は Sendable ではない
var body: some View {
EmptyView()
.task {
await repository.load() // ❌ データ競合の可能性
}
}
}
class Repository {
func load() async { /* 処理をする */ }
}
このコードでは、ContentView
が @MainActor
に隔離されていますが、Task
内の処理は新しい並行コンテキストで実行されます。
Sendable
に準拠していない Repository
インスタンスが異なるコンテキスト間でアクセスされるため、コンパイラがデータ競合のリスクを検出してコンパイルエラーを発生させます。
これに対して、いくつかの解決方法が考えられます。
1. 呼び出し先を @MainActor
に隔離する
呼び出し先へのアクセスを呼び出し元のアクター (この場合は @MainActor
) に制限することで、データ競合を防ぎます。
@MainActor
class Repository { // クラス全体を `@MainActor` に隔離する
func load() async { /* */ }
}
// または
class Repository {
@MainActor // 特定の関数のみを @MainActor に隔離する
func load() async { /* */ }
}
2. Repository
を Sendable
に準拠させる
Repository
を Sendable
に準拠させることで、インスタンスが並行コンテキスト間で安全に共有できることを保証します。
final class Repository: Sendable { // Sendableに準拠させる
// プロパティも全てSendableである必要がある
func load() async { /* */ }
}
// または
actor Repository { // actorとして定義することでSendableに準拠し、内部状態へのアクセスが排他的になる
func load() async { /* */ }
}
3. キャプチャリストを利用して値を分離する
クロージャのキャプチャリストを利用して、repository
の参照をクロージャ内のローカルコピーとして扱います。
これにより、異なる並行コンテキスト間で同じインスタンスが共有されるのを避け、そのタスク内でのみ有効な独立した参照として扱うことで、データ競合のリスクを回避できます。
この方法は、一時的に非 Sendable
なオブジェクトを並行コンテキストで使用する場合に有効ですが、オブジェクトのライフサイクルや共有状態の管理に注意が必要です。
var body: some View {
EmptyView()
.task { [repository] in // repositoryをキャプチャし、クロージャ内で独立して使用する
await repository.load()
}
}
これらの方法は、コードの設計やパフォーマンス要件に応じて使い分ける必要があります。
特に、重たい処理をメインスレッドで実行してしまうことは、UIのフリーズなどを引き起こすため注意が必要です。
Swift 6.2 で登場する nonisolated(nonsending)
とは
Swift 6.1 以前では、呼び出し元のアクターで実行させたい場合に、明示的に @MainActor
などを付与する必要がありました。
Swift 6.2 から導入される nonisolated(nonsending)
キーワードは、この挙動をより簡潔に記述できるようにします。
nonisolated(nonsending)
が付与された async
関数は、呼び出し元のアクターを継承して実行されます。
struct ContentView: View {
let repository = Repository()
var body: some View {
EmptyView()
.task {
await repository.load() // エラーが発生しない
}
}
}
class Repository { // Sendable でなくても OK
// ↓ 追加
nonisolated(nonsending) func load() async {
// 呼び出し元アクターで動く (この場合は MainActor)
}
}
上記の例では、ContentView
の .task
内から repository.load()
を呼び出しています。
.task
内のクロージャは、暗黙的に @MainActor
のコンテキストで実行されるため、nonisolated(nonsending)
が付与された load()
メソッドも @MainActor
上で実行されます。
もし load()
が別のアクターから呼ばれた場合は、そのアクターのコンテキストで実行されます。
この機能により、呼び出し元のアクターのコンテキストを維持したまま async
関数を実行したい場合に、アクター属性の指定が不要になります。
Xcode 26 のビルド設定 nonisolated(nonsending) By Default
について
Xcode 26 から登場したビルド設定 nonisolated(nonsending) By Default
は、上述の nonisolated(nonsending)
を暗黙的に追加するオプションです。
これにより、呼び出し元のアクターを継承させたい場合に毎回 nonisolated(nonsending)
を記述する手間が省けます。
ただし副作用として、この設定が有効な場合、連鎖するすべての呼び出し先が nonisolated(nonsending)
となります。
そのため、UI層から呼び出すとほぼすべての処理が @MainActor
で実行され、重い処理がメインアクターで実行される可能性があります。
これに対し、@concurrent
を使うことで解決が可能です。
@concurrent
で明示的に並列化する
nonisolated(nonsending) By Default
が有効になっている場合でも、メインアクターで実行したくない時間のかかる重い処理は、明示的に並列実行させる必要があります。
そのためのアノテーションが @concurrent
です。
struct ContentView: View {
let repository = Repository()
var body: some View {
EmptyView()
.task {
await repository.load()
}
}
}
@MainActor
class Repository {
@concurrent // async 関数に @concurrent をつける
func load() async {
// メインスレッド以外で実行される
}
}
上記の例では Repository
に @MainActor
を付与していますが、Sendable
に準拠させても問題ありません。
@concurrent
を適切に利用することで、nonisolated(nonsending) By Default
の利点を享受しつつ、アプリケーションのパフォーマンスを維持することができます。
まとめ
Swift 6.2 から導入される nonisolated(nonsending)
と @concurrent
、そして Xcode 26 の新しいビルド設定 nonisolated(nonsending) By Default
により、並行処理のコードがより書きやすくなります。
具体的には、これらの機能により以下が可能になります。
-
async
関数はデフォルトで呼び出し元のアクターで実行 - 重い処理には
@concurrent
を使用してバックグラウンド実行を明示的に指定
これらの改善により、Swift 6 の厳格な並行処理チェックを保ちながら、より実用的なコードを記述できるようになります。
どの処理をどのアクターで実行すべきか、または新しい並行タスクで実行すべきかを見極めながら、これらの設定とアノテーションを効果的に活用していきたいです。