Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Heart of Swift Concurrency

Heart of Swift Concurrency

Avatar for Yuta Koshizawa

Yuta Koshizawa

September 14, 2026

More Decks by Yuta Koshizawa

Other Decks in Programming

Transcript

  1. 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
  2. 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が必要 } }
  3. 自身のプロパティやメソッドには同期的にアクセス可 actor Counter { var count: Int = 0 func

    countUp() { ... } func countUpTwice() { countUp() // 同期 countUp() // 同期 } }
  4. 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) // 同期 } }
  5. ミュータブルクラス final class Foo: Sendable { // var value: Int

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

    // 不変 } init(value: Int) { self.value = value } 可変状態を持たないのでデータ競合の原因とならない
  7. (純粋な)値型 struct Foo: Sendable { // var value: Int //

    可変でもOK } init(value: Int) { self.value = value } 渡すときにコピーされるのでそもそも共有されない ✅
  8. Actor actor Foo { // ✅ 自動的にSendable準拠 var value: Int

    // 可変でもOK } init(value: Int) { self.value = value } valueはSerial Executorで保護されている
  9. ロック等で可変状態が保護されたミュータブルクラス final class Foo: Sendable { // private let mutex:

    Mutex<Int> var value: Int { get { mutex.withLock { $0 } } set { mutex.withLock { $0 = newValue } } } } init(value: Int) { self.mutex = .init(value) }
  10. non-SendableがIsola(on Boundaryを越えていい例 let a: A = .init() let foo: Foo

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

    = .init() await a.accept(foo) // foo.value += 1 // ⚠ 越境してはいけない 後から使うとデータ競合を起こし得る ⛔
  12. 間接的に「作って渡すだけ」の場合 ⛔ await bar(Foo()) // 作って渡すだけ func bar(_ foo: Foo)

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

    Foo) async { // fooが呼び出し元でもう使用されないとわかる } await a.accept(foo)
  14. AVCaptureVideoDataOutputはcallback queueを要求 actor CameraProcessor { private let output: AVCaptureVideoDataOutput private

    lazy var delegate: VideoOutputDelegate = ... } private func configure() throws { // ... output.setSampleBufferDelegate( delegate, queue: queue // DispatchQueue ) }
  15. Delegate nonisolated final class VideoOutputDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { private weak

    let owner: CameraProcessor? } init(owner: CameraProcessor) { self.owner = owner }
  16. 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) // }
  17. callback queueをActorのSerial Executorにする actor CameraProcessor { private let queue =

    DispatchSerialQueue(label: "...") } nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() }
  18. 同じ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 ) }
  19. コンパイラは同じ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) // }
  20. assumeIsolatedだけでは解決しない func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from

    connection: AVCaptureConnection ) { owner?.assumeIsolated { isolatedOwner in // Sending 'sampleBuffer' risks // causing data races isolatedOwner.process(sampleBuffer) // } }
  21. 静的なIsola&onのチェックを局所的に外す func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from

    connection: AVCaptureConnection ) { nonisolated(unsafe) let sampleBuffer = sampleBuffer } owner?.assumeIsolated { isolatedOwner in isolatedOwner.process(sampleBuffer) // }
  22. Mutexで可変状態を保護する final class OldFatService: Sendable { // var value: Int

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

    value: Int { get { _value.withLock { $0 } } set { _value.withLock { $0 = newValue } } } private let _value: Mutex<Int> } // ...
  24. 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<Int> } // ...
  25. @koher • Heart of Swi- 執筆 • Swi- Zoomin' 主催

    • Swi- Digest 運営 • ムゲンノゲーム 開発 • Swi- × RealityKitで3Dモデリング →
  26. 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
  27. Swi$ 6のデフォルト struct FooView: View { @State private var model:

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

    = .init() } func loadFoo() async throws { try await model.load() // }