本記事は、フューチャーアドベントカレンダー2024の16日目です。
はじめに
HealthCare Innovation Group(HIG)1所属の橋本です。
2024年9月16日にXcode 16がリリースされました!
Xcode16より内蔵されているSwift 6より、データ競合のチェックが厳しくなり、データ競合が起こる可能性のあるコードが含まれていると、コンパイル時にエラーとなってしまいます。
今回は、Actor
用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて実際にどのような対応が必要になっていくるのかを見ていきます。
以下の記事もSwift Advent Calendar 2024の8日目としてフューチャー技術ブログに投稿していますので、Swift 6移行対応に興味のある方はぜひ、覗いてみてください。
環境
- OS: macOS Sequoia 15.1.1(24B91)
- Xcode: Version 16.2 (16C5032a)
- Swift: 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
本記事では、Swift6への移行準備する想定の記事となっており、以下の設定とする。
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
それぞれ、TARGETS > Build Settings から設定ができます。
-
SWIFT_STRICT_CONCURRENCY = complete
の設定箇所
-
SWIFT_VERSION = 5.0
の設定箇所
データ競合が起こる事例に対して、どのように解決していくか
今回は、WWDC2021の次のセッション内容をもとにデータ競合を防ぐ(=Strict Concurrecy対応)方法をみていきます。
データ競合が起こるときは、上記のセッションで次のように説明されています。
Data race occer when
・Two Thread concurrenclently access the same data
・One of them is a write
つまり、データ競合は
- 同じデータに、2つのスレッドから平行にアクセスする
- アクセスするときに、少なくとも1つが書き込みである
まずは、データ競合が起こる例を示す。
class Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
func run() {
let counter = Counter()
Task.detached {
print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
}
Task.detached {
print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
}
}
上記のようなコードを書くと、次のような警告メッセージがでます。
Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
(日本語訳) 非Sendable型 @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' の値が転送後にアクセスされました。後続のアクセスで競合が発生する可能性があります。これはSwift 6言語モードではエラーとなります。
このコードだと、「Swift 6モードへあげたとき、エラーになってしまいますよ」と教えてくれています。これがSWIFT_STRICT_CONCURRENCY = complete
による効果です。このように、Swift 5モードでSWIFT_STRICT_CONCURRENCY = complete
とすることでSwift6移行に向けて、改善しないといけない箇所を洗い出すことができます。
Actorを用いてデータ競合を解決する
次のWWDC2021のセッション内容を参考にしています。
Actorとは
Actor
は共有可変状態のための同期メカニズムです。すなわち、Actor
はActor
インスタンスごとに残りのプログラムから隔離することで、データへの同期アクセスを実現しています。
Actor
インスタンスごとに隔離することができる(Isolation domain)を持ち、隔離したIsolation domain内のデータにアクセスをするには、Actorを経由するため、並行にアクセスをされないことをActor
が保証しています。
Actor
は、Class
と同様に参照型で、Swiftにおける型と同様の機能が提供されています。プロパティ、メソッド、イニシャライザ、サブスクリプトの機能を持ち、プロトコルに準拠させたり、エクステンションで拡張することもできます。
詳細は、次のProposal: SE-0306を確認してみてください。
Actorを使ってmutable Stateを保護する
さきほどのデータに競合が起こる可能性のあるコードをActorを用いて、改善していきます。
- class Counter {
+ actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
Counter
をactor
として、定義することでvalue
プロパティへ並列にアクセスされることから保護することができます。これは、actor
がSendable
に準拠しているため、並行タスクの間で安全に共有が可能になっています。
class
をactor
に変えたことにより、次のエラーが発生します。
Expression is 'async' but is not marked with 'await'
これは、actor
内のメソッドは非同期メソッドとして扱われるため、呼び出し時にawait
をつけずに呼び出しているためエラーとなっています。
func run() {
let counter = Counter()
Task.detached {
print(counter.increment()) // Expression is 'async' but is not marked with 'await'
}
Task.detached {
print(counter.increment()) // Expression is 'async' but is not marked with 'await'
}
}
エラーメッセージの内容に従って、await
をメソッドにつけます。
func run() {
let counter = Counter()
Task.detached {
- print(counter.increment()) // エラー発生
+ print(await counter.increment())
}
Task.detached {
- print(counter.increment()) // エラー発生
+ print(await counter.increment())
}
}
以上で、Swift Concurrency Checking
の警告メッセージは消え、Swift 6でもエラーが出ない形に修正が完了しました。
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
func run() {
let counter = Counter()
Task.detached {
print(await counter.increment())
}
Task.detached {
print(await counter.increment())
}
}
さいごに
今回は、一つのActor
用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて、実際にどのように対応するかを振り返りました。
データ競合が起こり得る箇所は、コンパイル時に潰すことができるようになるのは最終的には意図しないクラッシュや不具合等の数を大きく減らすことができる期待できるため、ユーザーの離脱を減らすことにつながるため、ビジネス的にも大変有用だと考えています。
Swift 6移行に取り組む際の参考になれば幸いです。