Slide 1

Slide 1 text

Heart of Swi, Concurrency @koher

Slide 2

Slide 2 text

Heart of Swi,

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Value Seman*cs Protocol-oriented Programming

Slide 5

Slide 5 text

Heart of Swi,

Slide 6

Slide 6 text

Heart of Swi, Concurrency

Slide 7

Slide 7 text

Swi$ Concurrencyは難しい?

Slide 8

Slide 8 text

Concurrencyの仕様は膨大で複雑

Slide 9

Slide 9 text

しかし、そのHeartはシンプル

Slide 10

Slide 10 text

Heartを押さえれば見通しがよくなる

Slide 11

Slide 11 text

Heart of Swi, Concurrency

Slide 12

Slide 12 text

Swi$ ConcurrencyのHeartとは?

Slide 13

Slide 13 text

Data Race Safety

Slide 14

Slide 14 text

コンパイラがデータ競合を防止すること

Slide 15

Slide 15 text

Isola&on Domain Sendable

Slide 16

Slide 16 text

データ競合とは?

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

データ競合が起こると • 未定義動作 • データ不整合 • クラッシュ • …

Slide 19

Slide 19 text

データ競合によるバグには対処しづらい • 問題を再現することが困難 • 原因を突き止めるのが困難 • コードを修正して解決されたことの確認が困難

Slide 20

Slide 20 text

データ競合を機械的に防止できるとうれしい

Slide 21

Slide 21 text

Strict Concurrency Checking

Slide 22

Slide 22 text

原因不明のクラッシュのほとんどが データ競合によって引き起こされていた例も

Slide 23

Slide 23 text

Swi$コンパイラは どのようにデータ競合を防ぐのか

Slide 24

Slide 24 text

Swi$コンパイラは 型を使ってデータ競合を防ぐ

Slide 25

Slide 25 text

Isola&on Domain Sendable

Slide 26

Slide 26 text

Isola&on Domain

Slide 27

Slide 27 text

Isola&on Domainとは?

Slide 28

Slide 28 text

可変状態へのアクセスを 隔離して保護する単位

Slide 29

Slide 29 text

Isola&on Domainのカテゴリー all func&on and variable declara&ons have a well-defined sta&c isola&on domain. These domains will always fall into one of three categories: 1. Non-isolated 2. Isolated to an actor value 3. Isolated to a global actor 2 "Migra)ng to Swi/ 6" h2ps://www.swi/.org/migra)on/documenta)on/swi/-6-concurrency-migra)on-guide/ dataracesafety#Isola)on-Domains

Slide 30

Slide 30 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 31

Slide 31 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 32

Slide 32 text

ActorとIsola*on Domain actor Counter { // 可変状態countはCounterインスタンスに隔離されて保護される var count: Int = 0 } func countUp() { count += 1 }

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Actor以前も同じようなことを DispatchQueueでやっていた

Slide 35

Slide 35 text

Actorはそれを自動的にやってくれる

Slide 36

Slide 36 text

外部からActorにアクセスするにはawaitが必要 let counter: Counter = .init() await counter.countUp() // awaitが必要 print(await counter.count) // awaitが必要

Slide 37

Slide 37 text

ActorのIsola*on Domainはインスタンスごとに存在 let a: Counter = .init() // aのIsolation Domain let b: Counter = .init() // bのIsolation Domain

Slide 38

Slide 38 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 39

Slide 39 text

ActorのIsola*on Domainはインスタンスごとに存在 actor Counter { var count: Int = 0 } func countUp() { ... }

Slide 40

Slide 40 text

ActorのIsola*on Domainはインスタンスごとに存在 actor Counter { var count: Int = 0 func countUp() { ... } func foo() async { let counter: Counter = .init() await counter.countUp() // awaitが必要 print(await counter.count) // awaitが必要 } }

Slide 41

Slide 41 text

自身のプロパティやメソッドには同期的にアクセス可 actor Counter { var count: Int = 0 func countUp() { ... } func countUpTwice() { countUp() // 同期 countUp() // 同期 } }

Slide 42

Slide 42 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 43

Slide 43 text

Global ActorとIsola-on Domain @MainActor final class FooViewModel { var value: Int = 42 }

Slide 44

Slide 44 text

Global ActorとIsola-on Domain @MainActor final class FooViewModel { var value: Int = 42 } // 暗黙的に@MainActor final class FooViewController: UIViewController { let viewModel: FooViewModel = .init() func buttonPressed() { print(viewModel.value) // 同期 } }

Slide 45

Slide 45 text

Global Actorはインスタンスを超えて Isola&on Domainを形成する

Slide 46

Slide 46 text

Global Actorは種類ごとに 単一のIsola-on Domainを作る

Slide 47

Slide 47 text

Global Actorは種類ごとに 単一のSerial Executorを持つ

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 50

Slide 50 text

TaskとIsola&on Domain Task { // Task独自のnon-isolatedなIsolation Domain }

Slide 51

Slide 51 text

non-isolatedなのにIsola,on Domain?

Slide 52

Slide 52 text

TaskとIsola&on Domain Task { // Task独自のnon-isolatedなIsolation Domain var value: Int = 42 }

Slide 53

Slide 53 text

一つのTaskの中からアクセスする限り 可変状態はデータ競合を起こさない

Slide 54

Slide 54 text

TaskとIsola&on Domain Task { // Task独自のnon-isolatedなIsolation Domain }

Slide 55

Slide 55 text

TaskとIsola&on Domain @MainActor func foo() { // MainActor-isolated Task { // MainActor-isolated } }

Slide 56

Slide 56 text

TaskとIsola&on Domain @MainActor func foo() { // MainActor-isolated Task.detached { // Task独自のnon-isolatedなIsolation Domain } }

Slide 57

Slide 57 text

Isola&on Domainのカテゴリー すべての関数宣言と変数宣言には、静的に定まるIsola&on Domain があります。Isola&on Domainは必ず、次の3つのカテゴリーのい ずれかに分類されます。 1. 隔離されていない(Non-isolated) 2. Actorの値に隔離されている 3. Global Actorに隔離されている

Slide 58

Slide 58 text

可変状態を単一のIsola&on Domainで 扱っている分にはデータ競合は起こらない

Slide 59

Slide 59 text

Isola&on Domain間で 値をやりとりできないと困る

Slide 60

Slide 60 text

Isola&on Domain間で共有しても 安全な値だけをやりとりできるようにする

Slide 61

Slide 61 text

安全な値とそうでないものを区別したい

Slide 62

Slide 62 text

Sendable

Slide 63

Slide 63 text

共有しても安全な値の型を Sendableプロトコル準拠で区別する

Slide 64

Slide 64 text

ミュータブルクラス final class Foo: Sendable { // var value: Int // 可変 } init(value: Int) { self.value = value } 同時にvalueにアクセスされるとデータ競合の原因となり得る

Slide 65

Slide 65 text

イミュータブルクラス final class Foo: Sendable { // let value: Int // 不変 } init(value: Int) { self.value = value } 可変状態を持たないのでデータ競合の原因とならない

Slide 66

Slide 66 text

(純粋な)値型 struct Foo: Sendable { // var value: Int // 可変でもOK } init(value: Int) { self.value = value } 渡すときにコピーされるのでそもそも共有されない ✅

Slide 67

Slide 67 text

Actor actor Foo { // ✅ 自動的にSendable準拠 var value: Int // 可変でもOK } init(value: Int) { self.value = value } valueはSerial Executorで保護されている

Slide 68

Slide 68 text

ロック等で可変状態が保護されたミュータブルクラス final class Foo: Sendable { // private let mutex: Mutex var value: Int { get { mutex.withLock { $0 } } set { mutex.withLock { $0 = newValue } } } } init(value: Int) { self.mutex = .init(value) }

Slide 69

Slide 69 text

Isola&on Domain間で共有されても安全な 値の型をSendableで区別できた

Slide 70

Slide 70 text

non-Sendableな値が Isola(on Domain間で共有されなければ Data Race Safetyを保証できる

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

Isola&on Boundaryの例 異なるIsola&on Domain間で • 関数やメソッドの引数に値を渡す • 戻り値として値を受け取る • クロージャに値をキャプチャする • …

Slide 73

Slide 73 text

non-Sendableな値が Isola(on Boundaryを越えようとしていたら コンパイラがエラーにする

Slide 74

Slide 74 text

コンパイラがData Race Safetyを保証

Slide 75

Slide 75 text

Sendable準拠という型の情報を使って 巧妙にData Race Safetyを実現

Slide 76

Slide 76 text

しかし、このチェックは保守的すぎる

Slide 77

Slide 77 text

Isola&on Domain間で共有されない ≠ Isola&on Boundaryを越えない

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

non-SendableがIsola(on Boundaryを越えていい例 final class Foo { // non-Sendable var value: Int = 42 }

Slide 80

Slide 80 text

non-SendableがIsola(on Boundaryを越えていい例 actor A { var foo: Foo } func accept(_ foo: Foo) { self.foo = foo }

Slide 81

Slide 81 text

non-SendableがIsola(on Boundaryを越えていい例 let a: A = .init() let foo: Foo = .init() await a.accept(foo) // ✅ 作って渡すだけ

Slide 82

Slide 82 text

non-SendableがIsola(on Boundaryを越えてはダメな例 let a: A = .init() let foo: Foo = .init() await a.accept(foo) // foo.value += 1 // ⚠ 越境してはいけない 後から使うとデータ競合を起こし得る ⛔

Slide 83

Slide 83 text

3 Region-based Isola/on 3 SE-0414: h+ps://github.com/swi9lang/swi9-evolu?on/blob/main/proposals/0414-region-based-isola?on.md

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

間接的に「作って渡すだけ」の場合 ⛔ await bar(Foo()) // 作って渡すだけ func bar(_ foo: Foo) async { // fooが呼び出し元でもう使用されないか判断できない } await a.accept(foo)

Slide 87

Slide 87 text

間接的に「作って渡すだけ」の場合 ✅ await bar(Foo()) // 作って渡すだけ func bar(_ foo: sending Foo) async { // fooが呼び出し元でもう使用されないとわかる } await a.accept(foo)

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

これまでのまとめ • Isola'on Domainが可変状態を隔離して保護 • 原則的にはSendableだけがIsola'on Boundaryを越えられる • 安全な場合にはnon-Sendableな値もBoundaryを越えられる • コンパイラが越境をチェックしてData Race Safetyを実現

Slide 91

Slide 91 text

Concurrencyと上手く付き合う方法

Slide 92

Slide 92 text

! Concurrencyに適したアプリの設計

Slide 93

Slide 93 text

MainActorでやって困らないことは MainActorでやる

Slide 94

Slide 94 text

16.7ミリ秒未満ですべての処理が終われば ユーザーは認識できない(60FPSの場合)

Slide 95

Slide 95 text

不必要に色んなものをActorにして 難しくしない

Slide 96

Slide 96 text

色んなものがActorになると Isola&on Boundaryを越えるときに • 非同期になりawaitが必要になったけど同期で反映したい • Sendableにしないとエラーが出るけど準拠が難しい

Slide 97

Slide 97 text

MainActor外でやるべき処理 • CPUバウンドな重い処理 • • 高頻度で受け取るデータの処理 • • 画像処理、巨大なデータのデコードなど … カメラ等のセンサーからの継続的な入力の解析など

Slide 98

Slide 98 text

! 遭遇しがちなエラーの対処法

Slide 99

Slide 99 text

例1: ActorとDispatchQueueの衝突

Slide 100

Slide 100 text

カメラの各フレームを順番にActorで処理 actor CameraProcessor { fileprivate func process( _ sampleBuffer: CMSampleBuffer ) { // 画像処理 } }

Slide 101

Slide 101 text

AVCaptureVideoDataOutputはcallback queueを要求 actor CameraProcessor { private let output: AVCaptureVideoDataOutput private lazy var delegate: VideoOutputDelegate = ... } private func configure() throws { // ... output.setSampleBufferDelegate( delegate, queue: queue // DispatchQueue ) }

Slide 102

Slide 102 text

Delegate nonisolated final class VideoOutputDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { private weak let owner: CameraProcessor? } init(owner: CameraProcessor) { self.owner = owner }

Slide 103

Slide 103 text

DelegateメソッドはActorに隔離されていない func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection ) { // Call to actor-isolated instance method 'process' // in a synchronous nonisolated context owner?.process(sampleBuffer) // }

Slide 104

Slide 104 text

callback queueをActorのSerial Executorにする actor CameraProcessor { private let queue = DispatchSerialQueue(label: "...") } nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() }

Slide 105

Slide 105 text

同じqueueをDelegateのcallback queueにする actor CameraProcessor { private let output: AVCaptureVideoDataOutput private lazy var delegate: VideoOutputDelegate = ... } private func configure() throws { // ... output.setSampleBufferDelegate( delegate, queue: queue // DispatchSerialQueue ) }

Slide 106

Slide 106 text

コンパイラは同じqueue上でも認識できない func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection ) { // Call to actor-isolated instance method 'process' // in a synchronous nonisolated context owner?.process(sampleBuffer) // }

Slide 107

Slide 107 text

assumeIsolatedだけでは解決しない func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection ) { owner?.assumeIsolated { isolatedOwner in // Sending 'sampleBuffer' risks // causing data races isolatedOwner.process(sampleBuffer) // } }

Slide 108

Slide 108 text

静的なIsola&onのチェックを局所的に外す func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection ) { nonisolated(unsafe) let sampleBuffer = sampleBuffer } owner?.assumeIsolated { isolatedOwner in isolatedOwner.process(sampleBuffer) // }

Slide 109

Slide 109 text

ConcurrencyのHeartを理解していれば 何が問題でエラーが起こっているのか 対処の方向性に問題がないか判断できる

Slide 110

Slide 110 text

例2: ミュータブルクラスを Sendableに準拠させたい

Slide 111

Slide 111 text

古い設計で解体するのが困難なミュータブルクラス final class OldFatService { var value: Int // ... }

Slide 112

Slide 112 text

ミュータブルなのでSendable準拠できない final class OldFatService: Sendable { // var value: Int // ... }

Slide 113

Slide 113 text

Mutexで可変状態を保護する final class OldFatService: Sendable { // var value: Int { get { _value.withLock { $0 } } set { _value.withLock { $0 = newValue } } } private let _value: Mutex } // ...

Slide 114

Slide 114 text

Mutexで可変状態を保護する import Synchronization final class OldFatService: Sendable { // var value: Int { get { _value.withLock { $0 } } set { _value.withLock { $0 = newValue } } } private let _value: Mutex } // ...

Slide 115

Slide 115 text

OSAllocatedUnfairLock (iOS 16, 17) import os final class OldFatService: Sendable { // var value: Int { get { _value.withLock { $0 } } set { _value.withLock { $0 = newValue } } } private let _value: OSAllocatedUnfairLock } // ...

Slide 116

Slide 116 text

! どうやって対処法を見つければいいか

Slide 117

Slide 117 text

AIを上手に活用

Slide 118

Slide 118 text

ローカルにcloneすべきリポジトリ AIからローカルでこれらのリポジトリを読めるようにする。 • swiftlang/swift-evolution • swiftlang/swift-migration-guide • swiftlang/swift-book

Slide 119

Slide 119 text

Xcode MCPのDocumenta0onSearch

Slide 120

Slide 120 text

! おすすめのビルド設定

Slide 121

Slide 121 text

Xcode Build Se,ngs

Slide 122

Slide 122 text

Package.swi+ // swift-tools-version: 6.4 .target( name: "...", swiftSettings: [ .enableUpcomingFeature("NonisolatedNonsendingByDefault"), .enableUpcomingFeature("InferIsolatedConformances"), ] ),

Slide 123

Slide 123 text

@koher • Heart of Swi- 執筆 • Swi- Zoomin' 主催 • Swi- Digest 運営 • ムゲンノゲーム 開発 • Swi- × RealityKitで3Dモデリング →

Slide 124

Slide 124 text

No content

Slide 125

Slide 125 text

No content

Slide 126

Slide 126 text

Isola&on Domain = Concurrency Domain

Slide 127

Slide 127 text

What is the difference between isola2on domain and concurrency domain? I'm leaning toward standardizing on "concurrency domain" and upda7ng the migra7on guide to use that term instead of "isola7on 1 domain" — Holly Borla (Mar 2, 2025) 1 h$ps://github.com/swi3lang/swi3-migra9on-guide/issues/130#issuecomment-2692364765

Slide 128

Slide 128 text

Isola&on DomainとConcurrency Domainの 違いは何ですか? 私は、用語を「Concurrency Domain」に統一し、Migra0on Guide でも「Isola0on Domain」の代わりにそれを使うよう更新する案に 傾いています。 — Holly Borla (Mar 2, 2025)

Slide 129

Slide 129 text

結局今もIsola&on Domainが優勢

Slide 130

Slide 130 text

No content

Slide 131

Slide 131 text

Swi$ 6のデフォルト @Observable final class FooModel { // non-isolated func load() async throws { // ... } }

Slide 132

Slide 132 text

Swi$ 6のデフォルト struct FooView: View { @State private var model: FooModel = .init() } func loadFoo() async throws { try await model.load() // }

Slide 133

Slide 133 text

Swi$ 6のデフォルト @Observable final class FooModel { // non-isolated nonisolated(nonsending) func load() async throws { // ... } }

Slide 134

Slide 134 text

NonisolatedNonsendingByDefault @Observable final class FooModel { // non-isolated } func load() async throws { // ... }

Slide 135

Slide 135 text

NonisolatedNonsendingByDefault struct FooView: View { @State private var model: FooModel = .init() } func loadFoo() async throws { try await model.load() // }