この記事の要点
- Swift 5.10 が正式リリースされました。このリリースの中心は、並行処理モデルにおける full data isolation(完全なデータ隔離) の達成です。
-strict-concurrency=completeを有効にすると、言語のあらゆる場面でデータ競合の可能性をコンパイル時に診断できるようになりました。 - データ競合チェックを部分的に無効化するための新しい
nonisolated(unsafe)キーワードが導入され、@unchecked Sendableよりも細かい単位で安全性チェックを抜けられるようになりました。 - Swift 5.10 の complete concurrency checking はまだ過剰に厳しく、安全なコードでも誤検知(false positive)が出ることがあります。次のメジャーリリースである Swift 6 に向けて、その使い勝手を改善する作業が進められています。
主な変更点
full data isolation の達成
Swift は当初から安全であることを重視して設計されており、変数の初期化前使用や use-after-free といった C 系言語での未定義動作の多くを言語仕様として排除してきました。並行処理において重要な未定義動作の発生源が、あるスレッドがメモリを書き込んでいる間に別のスレッドが同じメモリへアクセスしてしまう データ競合(data race) です。Swift はアクターとタスクによる data isolation でこの問題を解決し、共有された可変状態への相互排他的なアクセスを保証します。
Swift 5.10 は、この並行処理モデルにおける full data isolation を完成させました。並行処理モデルは Swift 5.5 で async/await・アクター・structured concurrency とともに導入され、Swift 5.7 で、スレッドセーフな型を表す基礎概念として Sendable が加わりました。そして Swift 5.10 では、complete concurrency checking を有効にしたとき、言語のすべての領域で full data isolation がコンパイル時に強制されるようになりました。
-strict-concurrency=complete を付けてビルドすると、nonisolated(unsafe) や @unchecked Sendable のような明示的なオプトアウトを使う場合を除いて、データ競合の可能性がコンパイル時に診断されます。たとえば次のコードは、Swift 5.9 では実行時に isolation アサーションで失敗するものの、-strict-concurrency=complete でも診断されませんでした。
@MainActor
class MyModel {
private init() {
MainActor.assertIsolated()
}
static let shared = MyModel()
}
func useShared() async {
let model = MyModel.shared
}
await useShared()
MyModel.shared は @MainActor に isolate された static 変数で、初回アクセス時に @MainActor 上で初期値が評価されるべきものです。しかし useShared() 内の nonisolated な文脈から同期的にアクセスされるため、初期値がメインアクター外で計算されてしまい、データ競合を許してしまいます。Swift 5.10 では、-strict-concurrency=complete でこのコードをコンパイルすると、アクセスを非同期に行うべきだという警告が出ます。
warning: expression is 'async' but is not marked with 'await'
let model = MyModel.shared
^~~~~~~~~~~~~~
await
このデータ競合を解消する方法は、1) await を使って MyModel.shared へ非同期にアクセスする、2) MyModel.init と MyModel.shared をどちらも nonisolated にし、メインアクターを必要とする処理を別の isolated なメソッドに移す、3) useShared() 自体を @MainActor に isolate する、のいずれかです。
nonisolated(unsafe) による細かいオプトアウト
@unchecked Sendable のようなオプトアウトは、コンパイラが自動的には安全性を証明できないものの、実際にはデータ競合がないことを伝えるために重要です。OS 固有のプリミティブによる同期や、C / C++ / Objective-C で実装されたスレッドセーフな型を扱う場合など、コンパイラが推論できない方法で同期が実装されているケースで必要になります。しかし @unchecked Sendable は型全体をデータ競合安全性のチェックから外してしまうため、正しく使うのが難しいという問題がありました。多くの場合、オプトアウトが必要なのは型の中の特定のプロパティ 1 つだけで、残りの実装は静的な並行処理安全性に従っているからです。
Swift 5.10 では、stored property や変数について actor isolation チェックを無効化する新しい nonisolated(unsafe) キーワードが導入されました。nonisolated(unsafe) は、stored property・ローカル変数・グローバル/static 変数を含む、あらゆる形式の格納に使えます。
たとえばグローバル変数や static 変数はコードのどこからでもアクセスできるため、イミュータブルかつ Sendable であるか、グローバルアクターに isolate されている必要があります。
import Dispatch
struct MyData {
static let cacheQueue = DispatchQueue(...)
// 'globalCache' へのアクセスはすべて 'cacheQueue' で守られている
static var globalCache: [MyData] = []
}
このコードを -strict-concurrency=complete でビルドすると、globalCache が isolate されていないグローバルな共有可変状態であるという警告が出ます。実際には globalCache へのアクセスはすべて cacheQueue.async { ... } で守られているためデータ競合は起きません。この場合、static 変数に nonisolated(unsafe) を付けることで警告を抑制できます。
import Dispatch
struct MyData {
static let cacheQueue = DispatchQueue(...)
// 'globalCache' へのアクセスはすべて 'cacheQueue' で守られている
nonisolated(unsafe) static var globalCache: [MyData] = []
}
nonisolated(unsafe) を使うと、non-Sendable な値を isolation boundary を越えて渡すためだけに用意していた @unchecked Sendable のラッパー型も不要になります。並行アクセスの可能性がない場面で、特定のインスタンスを越境させたいだけのケースに有効です。
// 'MutableData' は 'Sendable' ではない
class MutableData { ... }
func processData(_: MutableData) async { ... }
@MainActor func send() async {
nonisolated(unsafe) let data = MutableData()
await processData(data)
}
なお、unsafe という名前のとおり、data isolation を達成するための同期機構が正しく実装されていなければ、排他的アクセスの動的チェックや Thread Sanitizer などのツールが、依然として実行時に問題を検出する可能性があります。
Swift 6 に向けた言語進化
次のリリースは Swift 6 です。Swift 5.10 の complete concurrency checking は過剰に厳しく、安全だと証明できるコードでもデータ競合の警告(false positive)が出ることがあります。Swift 6 に向けた開発の大きな焦点は、こうした誤検知を減らして full data isolation の使い勝手を高めることです。
この取り組みには、コンパイラが並行アクセスの可能性がないと判断できる場合に non-Sendable な値を isolation boundary を越えて渡せるようにする region based isolation(SE-0414)や、関数や key path に対する Sendable 推論の強化(SE-0418)などが含まれます。
Swift 6.0 コンパイラは、オプトインで有効にできる新しい Swift 6 言語モードを提供する予定で、このモードでは full data isolation がデフォルトで強制されます。complete concurrency checking を自分のプロジェクトで試し、フィードバックを送ることで、Swift 6 言語モードへの移行に貢献できます。
Swift 5.10 に含まれる主な Swift Evolution Proposal
Swift 5.10 で実装された言語 Proposal には次のものがあります。
- アクターと初期化(SE-0327)
@UIApplicationMain/@NSApplicationMainの非推奨化(SE-0383)- 非ジェネリックな文脈でのプロトコルのネスト(SE-0404)
- isolate されたデフォルト値式(SE-0411)
- グローバル変数に対する strict concurrency(SE-0412)
Swift 利用者への影響
- 並行処理コードの安全性をコンパイル時に検証できます。
-strict-concurrency=completeを有効にすると、言語のあらゆる場面でデータ競合の可能性が診断され、明示的なオプトアウトのない限りデータ競合のないコードであることを保証できます。 - 安全性チェックをより細かく制御できます。
nonisolated(unsafe)により、型全体ではなく特定の stored property・変数・ローカル変数の単位でチェックを抜けられます。@unchecked Sendableのラッパー型を用意する必要も減ります。 - Swift 6 への移行準備を始められます。 Swift 5.10 の complete concurrency checking はまだ誤検知が残りますが、いま試してフィードバックを送ることで、より使いやすい Swift 6 言語モードづくりに貢献できます。
関連 Proposal・リンク
- 前のリリースについては Swift 5.9 リリース を参照してください。
- 本記事で触れた Proposal ダイジェスト:
- Swift 5.10 に含まれる言語進化のProposal一覧: Swift Evolution dashboard
- Swift のダウンロード
- The Swift Programming Language