Swift Digest
SE-0412 | Swift Evolution

Strict concurrency for global variables

Proposal
SE-0412
Authors
John McCall, Sophia Poirier
Review Manager
Holly Borla
Status
Implemented (Swift 5.10)

01 何が問題だったのか

グローバル変数(グローバルスコープの let / stored var や、型の static メンバ)は、どの文脈からでもアクセスできてしまうため、データ競合の温床になりやすい存在です。

  • ローカル変数はそのスコープ内に閉じており、暗黙にisolateされます。
  • 値型の stored property は排他制御(exclusivity)のルールで守られます。
  • 参照型の stored property も、包む型を sendability やアクターで守れば isolate できます。

これに対してグローバル変数は、どこからでも直接触れるため、これらの仕組みがそのままでは効きません。その結果、次のようなコードを書いても、strict concurrency checking が無ければ警告なく通ってしまいます。

var value = 1

func f() {
  value = 2 // 異なるスレッドから同時に呼ばれるとデータ競合
}

Swift 6のデータ競合安全性を完成させるうえで、グローバル変数をどのように扱うかを決める必要がありました。

02 どのように解決されるのか

strict concurrency checking の下では、すべてのグローバル変数に次のいずれかを要求します。

  • グローバルアクターにisolateされている、または
  • 以下の両方を満たす
    • イミュータブル(let)である
    • Sendable な型である

イミュータブルかつ Sendable であればどこから読んでも安全で、それ以外の場合は isolation が必要、という整理です。グローバル変数の遅延初期化自体はもともとスレッドセーフが保証されているため、追加の指定は必要ありません。

トップレベル(スクリプトの最上位)のグローバル変数は、すでに暗黙に @MainActor にisolateされるため、この要件を自動的に満たします。

グローバルアクターでの isolation

var のグローバル変数で共有可変状態が必要な場合は、グローバルアクターを付与して isolate します。

@MainActor var counter = 0

counter への読み書きは @MainActor のドメインに限定されるため、他のドメインからは await を介してアクセスすることになります。

イミュータブルかつ Sendablelet

単なる共有定数であれば、letSendable 型を使うのが最も軽量な選択肢です。

let greeting: String = "Hello"

これはどの isolation ドメインからでも安全に読め、アクター指定は不要です。

nonisolated(unsafe) によるオプトアウト

自前のロックなどで同期を取っており、コンパイラの静的チェックを迂回したい場合のために、nonisolated(unsafe) という属性が用意されます。付けた変数(グローバル変数に限らず任意の storage に使えます)に対する静的な isolation チェックは抑制されますが、安全性を保証する責任は利用者側に移ります。排他制御(exclusivity)の実行時チェックや Thread Sanitizer などの動的解析では、依然として競合が検出され得ます。

nonisolated(unsafe) var global: String

同じ属性はローカル変数にも使えて、非同期文脈からローカル変数を触るときの診断を抑制できます。

func f() async {
  nonisolated(unsafe) var value = 1
  let task = Task {
    value = 2
    return value
  }
  print(await task.value)
}

なお nonisolated は文脈依存キーワードであるため、スクリプトのトップレベルで nonisolated(unsafe) を単体の行に書くと、unsafe を引数に取る nonisolated という関数呼び出しとも解釈でき得ます。単一の無ラベル引数 unsafe を持ち、直後に変数宣言が続く場合は、isolation 指定としての解釈を優先することで曖昧さを解決します。

@preconcurrency import されたモジュール

@preconcurrency import で取り込まれたモジュールのグローバル変数は、明示的な concurrency annotation が無くても isolation チェックによるエラーにはなりません。ただし、concurrency-unsafe なグローバル変数を使用すると、利用側で警告が出ます。

C や Objective-C など他言語からの import は暗黙に @preconcurrency として扱われます。これらの変数を安全に扱う手段としては、Cヘッダ側で __attribute__((swift_attr("@MainActor"))) のようにグローバルアクターでisolateする、あるいは適切なロックや isolation を持つ Swift 側のラッパー API で包む、といった方法があります。

有効化

この仕様は upcoming feature flag GlobalConcurrency の下で導入され、Swift 6 言語モードでは既定で有効になります。既存プロジェクトを Swift 6 に移行する際には、グローバル変数の型や isolation の見直しが必要になる場合があります。