放課後のプログラミング教室
1. 導入
ユウタ:「先生! Swiftで同時実行(Concurrency)の学習をしてるんですが、コンパイラがいろいろ文句を言ってきて大変なんです。『CommonProblems.md』という資料を見つけたんですけど、これ、どんなことが書かれているんでしょう?」
ミズ・スウィフト(先生):「いい質問ね。実はこの『CommonProblems.md』には、Swiftの並行処理を使ううえでよく遭遇するコンパイラエラーと、その原因と対処法が詳しく書かれているの。ちょうどいいから、アキラと一緒に、この資料を元に勉強してみましょうか。」
アキラ:「わあ、それは助かる! どんな内容なのか、早速教えてください!」
ミズ・スウィフト:「わかったわ。まずは資料の冒頭を見てみましょう。こう書かれているの。」
CommonProblems.md より抜粋
Common Compiler Errors
Identify, understand, and address common problems you can encounter while working with Swift concurrency.
The data isolation guarantees made by the compiler affect all Swift code. This means complete concurrency checking can surface latent issues, even in Swift 5 code that doesn't use any concurrency language features directly. With the Swift 6 language mode enabled, some of these potential issues can also become errors.
After enabling complete checking, many projects can contain a large number of warnings and errors. Don't get overwhelmed! Most of these can be tracked down to a much smaller set of root causes. And these causes, frequently, are a result of common patterns which aren't just easy to fix, but can also be very instructive while learning about Swift's concurrency system.
ミズ・スウィフト:「要するに、『Swift 6』でコンパイラのチェックが厳しくなって、これまで表に出なかった問題がエラーとして顕在化するかもしれない。でも心配しないで、よくある原因は意外と共通パターンがあるって話ね。」
アキラ:「確かに、前のバージョンのSwiftでは普通に動いていたコードが、急にエラーになったりするんですよね。」
ユウタ:「はい。じゃあ一つ一つ見ていきたいです!」
2. Unsafe Global and Static Variables
ミズ・スウィフト:「まずは『Unsafe Global and Static Variables』というセクション。グローバルな変数やstatic変数を乱用するとデータ競合が起こりやすいってことが書いてあるのよ。」
CommonProblems.md より抜粋
Unsafe Global and Static Variables
Global state, including static variables, are accessible from anywhere in a program. This visibility makes them particularly susceptible to concurrent access. Before data-race safety, global variable patterns relied on programmers carefully accessing global state in ways that avoided data-races without any help from the compiler.
Experiment: These code examples are available in package form. Try them out yourself in Globals.swift.
ユウタ:「なるほど、グローバル変数は色んなところから同時に触れちゃいますからね。」
ミズ・スウィフト:「そうそう。ここからさらに詳しい例が出ているわ。例えばこんなコードよ。」
var supportedStyleCount = 42
ミズ・スウィフト:「これをSwift 6モードでコンパイルすると、こんなエラーが出るの。」
1 | var supportedStyleCount = 42
| |- error: global variable 'supportedStyleCount' is not concurrency-safe because it is non-isolated global shared mutable state
| |- note: convert 'supportedStyleCount' to a 'let' constant to make the shared state immutable
| |- note: restrict 'supportedStyleCount' to the main actor if it will only be accessed from the main thread
| |- note: unsafely mark 'supportedStyleCount' as concurrency-safe if all accesses are protected by an external synchronization mechanism
2 |
アキラ:「コンパイラがかなり丁寧にヒントをくれていますね。let
にするといいとか、@MainActor
にするといいとか。」
ミズ・スウィフト:「そうなの。そして問題の例として、異なる隔離ドメイン(例えばメインアクターと別のスレッド)が同時に書き換えると、データ競合が起こり得るわ。」
@MainActor
func printSupportedStyles() {
print("Supported styles: ", supportedStyleCount)
}
func addNewStyle() {
let style = Style()
supportedStyleCount += 1
storeStyle(style)
}
ユウタ:「メインアクターで実行される関数と、そうでない関数が同じ変数を触ると危険ってことですね。」
ミズ・スウィフト:「正解。対策の一例としては、変数に@MainActor
を付けて、同じアクター上だけで扱うようにするか、あるいは値を変えないならlet
にしてしまうの。それぞれこう書いてあるわ。」
@MainActor
var supportedStyleCount = 42
または
let supportedStyleCount = 42
ユウタ:「なるほど、変更しないならlet
。変更するけどメインスレッドでしか使わないなら@MainActor
。それでも無理なら?」
ミズ・スウィフト:「もし、ロック(排他制御)などでちゃんと保護してますよ、という場合には、nonisolated(unsafe)
を使ってコンパイラチェックを無効化できるわ。でも本当に安全な場合のみね。」
/// This value is only ever accessed while holding `styleLock`.
nonisolated(unsafe) var supportedStyleCount = 42
アキラ:「これは『自分で責任を持つからチェックしないで!』という感じですね。乱用は危険そう。」
3. Non-Sendable Types
ミズ・スウィフト:「次に、ここで話されているのは、グローバルに公開されているのがSendable
じゃない型の場合よ。」
CommonProblems.md より抜粋
Non-Sendable Types
In the above examples, the variable is an
Int
, a value type that is inherentlySendable
. Global reference types present an additional challenge, because they are typically notSendable
.class WindowStyler { var background: ColorComponents static let defaultStyler = WindowStyler() }
The problem with this
static let
declaration is not related to the mutability of the variable. The issue isWindowStyler
is a non-Sendable
type, making its internal state unsafe to share across isolation domains.
ユウタ:「つまり、クラスは普通に非同期対応でない限りSendable
じゃないから、グローバルに置くと危ないってことですね。」
ミズ・スウィフト:「その通り。例えば以下みたいに使われた場合、データ競合が起こるかもしれないのよ。」
func resetDefaultStyle() {
WindowStyler.defaultStyler.background = ColorComponents(red: 1.0, green: 1.0, blue: 1.0)
}
@MainActor
class StyleStore {
var stylers: [WindowStyler]
func hasDefaultBackground() -> Bool {
stylers.contains { $0.background == WindowStyler.defaultStyler.background }
}
}
アキラ:「同時アクセスが怖いってことですね。」
ミズ・スウィフト:「そう。解決策としては、WindowStyler.defaultStyler
をグローバルアクターに隔離するか、WindowStyler
自体をSendable
にするか、いろいろあるわ。」
4. Protocol Conformance Isolation Mismatch
ミズ・スウィフト:「次に、プロトコルと実装側で隔離の指定が食い違う場合の話ね。」
CommonProblems.md より抜粋
Protocol Conformance Isolation Mismatch
A protocol defines requirements that a conforming type must satisfy, including static isolation. This can result in isolation mismatches between a protocol's declaration and conforming types.
There are many possible solutions to this class of problem, but they often involve trade-offs. Choosing an appropriate approach first requires understanding why there is a mismatch in the first place.
Experiment: These code examples are available in package form. Try them out yourself in [ConformanceMismatches.swift][ConformanceMismatches].
ユウタ:「プロトコルに@MainActor
をつけるか、実装側に合わせるか、とかの話ですかね?」
ミズ・スウィフト:「そうそう。例えばプロトコルが isolation を指定していないと、それは『非隔離』扱いになるの。なのに実装側が@MainActor
を付けていると、エラーになるわ。」
protocol Styler {
func applyStyle()
}
@MainActor
class WindowStyler: Styler {
func applyStyle() {
// access main-actor-isolated state
}
}
7 | func applyStyle() {
| |- error: main actor-isolated instance method 'applyStyle()' cannot be used to satisfy nonisolated protocol requirement
ミズ・スウィフト:「プロトコルに@MainActor
を付ければいい場合もあるし、非同期に変えて func applyStyle() async
にするなど、対処はいろいろあるわね。例えば、プロトコル全体をメインアクターにするのか、メソッド単位にするのか、とか。」
// entire protocol
@MainActor
protocol Styler {
func applyStyle()
}
// or per-requirement
protocol Styler {
@MainActor
func applyStyle()
}
アキラ:「どの方法がベストかは、使い方次第って感じですね。」
ミズ・スウィフト:「そうね。あと、既存のコードをすぐに変えられない場合は@preconcurrency
とかで一時的に診断を緩和する方法もあるわ。」
5. Crossing Isolation Boundaries
ユウタ:「先生、関数間で引数をやりとりするときに、『Sendable
じゃない!』って怒られることもあるんですが、これってどう対応するんですか?」
ミズ・スウィフト:「それが、資料の『Crossing Isolation Boundaries』に書いてあるわ。」
CommonProblems.md より抜粋
Crossing Isolation Boundaries
The compiler will only permit a value to move from one isolation domain to another when it can prove it will not introduce data races. Attempting to use values that do not satisfy this requirement in contexts that can cross isolation boundaries is a very common problem.
ミズ・スウィフト:「ポイントは、あるアクターやスレッドから別のアクターやスレッドにデータを送るとき、そのデータがSendable
かどうかをコンパイラがチェックしているってこと。もし非Sendable
なら怒られるの。」
ユウタ:「@MainActor
な関数に非Sendable
な引数を渡すと怒られました……」
ミズ・スウィフト:「例えばこういう例があるわ。」
public struct ColorComponents {
public let red: Float
public let green: Float
public let blue: Float
}
@MainActor
func applyBackground(_ color: ColorComponents) {
}
func updateStyle(backgroundColor: ColorComponents) async {
await applyBackground(backgroundColor)
}
8 | await applyBackground(backgroundColor)
| |- error: sending 'backgroundColor' risks causing data races
アキラ:「ColorComponents
がpublic
だから、暗黙のSendable
にならない、ってわけですね?」
ミズ・スウィフト:「そうなの。だから、ちゃんと public struct ColorComponents: Sendable { ... }
って明示する必要があるわ。もちろん、それをAPI契約として維持しなきゃいけなくなるから注意が必要だけどね。」
6. Latent Isolation
ユウタ:「非Sendable
を無理にSendable
にせずに済む方法もあるんですか?」
ミズ・スウィフト:「そう! 実は、関数自体を@MainActor
にするとか、そもそも引数を渡さなくて済むように設計を変えるとか、ほかにも手はあるのよ。」
@MainActor
func applyBackground(_ color: ColorComponents) {
}
func updateStyle(backgroundColor: ColorComponents) async {
await applyBackground(backgroundColor)
}
@MainActor
func updateStyle(backgroundColor: ColorComponents) async {
applyBackground(backgroundColor)
}
ミズ・スウィフト:「こうすると、引数の受け取り先も呼び出し元も、どちらもMainActor
だから、隔離境界を越えずに済むわけ。」
アキラ:「なるほど。非同期をなくせる場合もありそうですね。」
ミズ・スウィフト:「そうそう。」
7. Computed Value / Sending Argument
ミズ・スウィフト:「ほかに、@Sendable () -> T
なクロージャで値を作るだけにするとか、sending
キーワードを使って引数として安全に渡すとか、細かいテクニックもあるわ。」
func updateStyle(backgroundColorProvider: @Sendable () -> ColorComponents) async {
await applyBackground(using: backgroundColorProvider)
}
func updateStyle(backgroundColor: sending ColorComponents) async {
await applyBackground(backgroundColor)
}
ユウタ:「あ、sending
引数! Swift 6で増えた新しい書き方ですよね?」
ミズ・スウィフト:「そうなの。こうして型に直接Sendable
を付けずに、安全にデータを渡す工夫ができるわ。」
8. Sendable Conformance
ミズ・スウィフト:「次は『Sendable』への準拠をどうやって行うか、4つの方法がまとめられているから、一気に見てみましょう。」
CommonProblems.md より抜粋
When encountering problems related to crossing isolation domains, a very natural reaction is to just try to add a conformance to
Sendable
. You can make a typeSendable
in four ways.1. Global Isolation
@MainActor public struct ColorComponents { // ... }
By isolating this type to the MainActor, any accesses from other isolation domains must be done asynchronously.
2. Actors
actor Style { private var background: ColorComponents }
Actors have an implicit Sendable conformance because their properties are protected by actor isolation.
3. Manual Synchronization
class Style: @unchecked Sendable { private var background: ColorComponents private let queue: DispatchQueue }
4. Retroactive Sendable Conformance
extension ColorComponents: @retroactive @unchecked Sendable { }
...
Sendable Reference Types
final class Style: Sendable { private let background: ColorComponents }
ミズ・スウィフト:「要点をまとめると、(1)グローバルアクターにする、(2)アクターそのものにする、(3)手動で同期をとって@unchecked Sendable
にする、(4)レトロアクティブに適用する、の4つって感じね。」
アキラ:「それと、final
クラスかつ継承していないなど、いろいろ条件を満たせば、参照型もチェック付きSendable
にできるんですね。」
ミズ・スウィフト:「そう。あとは型を複合的に組み合わせて、部分的に@MainActor
やnonisolated(unsafe)
を使う、なんてことも紹介されてるわ。」
final class Style: Sendable {
private nonisolated(unsafe) var background: ColorComponents
private let queue: DispatchQueue
@MainActor
private var foreground: ColorComponents
}
9. Non-Isolated Initialization
ユウタ:「アクターにしたら、イニシャライザってどうなるんでしょう? 普通に初期化すると『非isolatedなコンテキストだ!』って怒られたりしますよね?」
ミズ・スウィフト:「そこもちゃんと資料に書いてあるわ。」
CommonProblems.md より抜粋
Non-Isolated Initialization
Actor-isolated types can present a problem when they are initialized in a non-isolated context.
Here the non-isolated
Stylers
type is making a call to aMainActor
-isolated initializer.@MainActor class WindowStyler { init() { } } struct Stylers { static let window = WindowStyler() }
This code results in the following error:
9 | static let window = WindowStyler() | `- error: main actor-isolated default value in a nonisolated context
Globally-isolated types sometimes don't actually need to reference any global actor state in their initializers. By making the
init
methodnonisolated
, it is free to be called from any isolation domain.
@MainActor
class WindowStyler {
private var viewStyler = ViewStyler()
private var primaryStyleName: String
nonisolated init(name: String) {
self.primaryStyleName = name
// type is fully-initialized here
}
}
ユウタ:「なるほど。イニシャライザだけnonisolated
にすれば、他のプロパティは@MainActor
のままでもOKなんですね。」
ミズ・スウィフト:「そういうこと。初期化のときは特別だから、うまく使い分けないといけないのよ。」
10. Non-Isolated Deinitialization
アキラ:「あ、そういえばデイニシャライザdeinit
も、アクターなのに並行アクセスの問題があるとか……」
ミズ・スウィフト:「そう。deinit
は常に非isolatedなの。だから中でアクター隔離されたメソッド呼ぶとエラーになるわ。」
CommonProblems.md より抜粋
Non-Isolated Deinitialization
Even if a type has actor isolation, deinitializers are always non-isolated.
actor BackgroundStyler { // another actor-isolated type private let store = StyleStore() deinit { // this is non-isolated store.stopNotifications() } }
This code produces the error:
error: call to actor-isolated instance method 'stopNotifications()' in a synchronous nonisolated context
Often, the work being done within the deinit does not need to be synchronous. A solution is to use an unstructured Task to first capture and then operate on the isolated values.
actor BackgroundStyler { private let store = StyleStore() deinit { Task { [store] in await store.stopNotifications() } } }
Important: Never extend the life-time of
self
from withindeinit
. Doing so will crash at runtime.
ミズ・スウィフト:「deinit
の中ではself
をキャプチャしないようにして、必要な処理はタスクの中でやる、っていうのがポイントね。」
アキラ:「気をつけないとですね。デイニシャライザは終了処理だからといって、自由に呼び出せるわけじゃないんだ……」
まとめ
ユウタ:「わあ、長かったですけど、これが『CommonProblems.md』の全部なんですね。いろんなケースがあって、すごく勉強になりました!」
ミズ・スウィフト:「そうでしょ? Swiftの同時実行では、データ競合を防ぐための仕組みとしてアクターやSendable
が導入されているから、コンパイラがいっぱい警告してくれるの。でも、それをどう対処するか、パターンを覚えれば怖くないわ。」
アキラ:「グローバル変数、クラスのSendable
、プロトコルの隔離、境界を越えた引数の受け渡し、イニシャライザとデイニシャライザ……一通り押さえておけば安心ですね!」
ミズ・スウィフト:「ええ。Swift 6の新しいルールに最初は戸惑うかもしれないけど、一つひとつは理にかなっているの。エラーが出ても、慌てずこうした資料を参考にして、正しく修正していきましょう。」
ユウタ:「はい、先生、ありがとうございました! これでデータ競合に悩まされずに済みそうです!」
アキラ:「僕も、さらにSwiftのコードを書くのが楽しみになりました!」
以上が、『CommonProblems.md』に書かれている内容のすべて(文章とコード例)を、高校生向けに物語形式でまとめたものです。各種サンプルコードとともに、エラー例や対処法が一通り整理されていますので、ぜひ参考にしてみてください。