【Swift】Sendableとは?データ競合を防ぐ仕組みを解説

Swift

Sendableとは

Sendable | Apple Developer Documentation
A thread-safe type whose values can be shared across arbitrary concurrent contexts without introducing a risk of data ra...

Sendableとは、Swift 5.5で導入されたプロトコルで、並行処理において安全に異なるタスクや実行コンテキスト間でやり取りできる型を表すことができます。

簡潔にいうと、コンパイラに「この型は並行処理で安全に使えるよ!」「データ競合起こらないよ!」と伝える役割を持っています。

今回はSendableが導入された背景、使い方について自分なりに調べたことをまとめていきます。

動作環境

  • macOS Tahoe 26.0
  • Xcode 26.0 beta

データ競合

データ競合とは、並行処理において1つのリソースに複数のタスクが同時にアクセスして、結果が意図しないものになってしまうことです。

たとえば、以下のコードではCounterのインスタンスに、2つのTaskが同時にアクセスしてデータ競合がおきています。

Swift
class Counter {
    var count = 0
    
    func increment() {
        count += 1
    }
}

let counter = Counter()

Task { 
    counter.increment()
    print(counter.count)
}

Task { 
    counter.increment()
    print(counter.count)
}

このコードを実行してみると、結果が(1, 2) (2, 1) (1, 1) (2, 2) のどれかになってしまいます。

これは2つのTaskがcounterの値を読み取るタイミング、書き換えるタイミングが重なってしまうことで起きています。

データ競合は結果の予測ができないだけじゃなく、意図しない値によってアプリがクラッシュしてしまうこともあります。

またテストだけでは発見しにくい問題もあります。

このデータ競合を解決するために導入されたのが、 Isolation domain という概念です。

Isolation domain

Documentation

Isolation domain(分離ドメイン)はデータ競合を防ぐために導入された概念です。

ここで分離されたタスクは1つずつ処理されて、データ競合は起こらないようになっています。

actor@MainActor@globalActor などを使って定義することができます。

先ほどのコードをactorを使って書き直してみましょう。

actor内のメソッドやプロパティにアクセスするには、awaitを使います。

Swift
import Foundation

actor Counter {
    var count = 0

    func increment() {
        count += 1
    }
}

let counter = Counter()

Task {
    await counter.increment() // actor内のメソッド、プロパティにアクセスするにはawaitが必須
    await print(counter.count)
}

Task {
    await counter.increment()
    await print(counter.count)
}

こちらのコードを実行すると常に、 (2, 2) になります。

actor内部では1つずつ順番に処理されるので、データ競合が起こる心配はありません。

しかし、以下の場合はどうなるでしょうか?

Swift
import Foundation

class Counter {
    var count = 0
}

actor ActorA {
    func process(_ data: Counter) {
        data.count += 1
    }
}

actor ActorB {
    func process(_ data: Counter) {
        data.count += 10
    }
}

let counter = Counter()
let actorA = ActorA()
let actorB = ActorB()

Task {
    await actorA.process(counter) // ❌ Sending 'counter' risks causing data races
    print(counter.count)
}

Task {
    await actorB.process(counter) // ❌ Sending 'counter' risks causing data races
    print(counter.count)
}

こちらのコードを実行しようとすると、 Sending 'counter' risks causing data races というエラーが発生します。

Isolation domainは内部でデータ競合がおきないことは保証しますが、異なるdomain間でのデータの受け渡しの安全は保証されていません。

そこで登場するのがSendableです。

Sendable

先述した通りSendableは、コンパイラに「この型はデータ競合おきないよ!」と伝えるプロトコルです。

言い換えると、異なるIsolation domainに渡してもデータ競合が起こらないことを保証してくれるものです。

先ほどのコードをSendableを使って書き直してみましょう。

actorは自動的にSendableに準拠されるので、actorを使っています。

Swift
import Foundation

actor Counter { // 自動的にSendableに準拠される
    var count = 0

    func increment() {
        count += 1
    }
}

actor ActorA {
    func process(_ data: Counter) async {
        await data.increment()
    }
}

actor ActorB {
    func process(_ data: Counter) async {
        await data.increment()
    }
}

let counter = Counter()
let actorA = ActorA()
let actorB = ActorB()

Task {
    await actorA.process(counter)
    await print(counter.count)
}

Task {
    await actorB.process(counter)
    await print(counter.count)
}

またはMutexを使って、以下のように書くこともできます。

Swift
import Foundation
import Synchronization

final class Counter: Sendable {
    private let _count = Mutex<Int>(0)

    func getCount() -> Int {
        _count.withLock { $0 }
    }

    func increment() {
        _count.withLock {
            $0 += 1
        }
    }
}

actor ActorA {
    func process(_ data: Counter) {
        data.increment()
    }
}

actor ActorB {
    func process(_ data: Counter) {
        data.increment()
    }
}

let counter = Counter()
let actorA = ActorA()
let actorB = ActorB()

Task {
    await actorA.process(counter)
    print(counter.getCount())
}

Task {
    await actorB.process(counter)
    print(counter.getCount())
}

Sendableの条件

Sendableに準拠させるには、それぞれ条件があります。

struct, enumの場合

struct, enumの場合、Sendableに準拠させるための条件は1つです。

  • 全てのプロパティ(enumはAssociated Value)がSendableであること

以下はSendableに準拠できている例です。

ちなみにstruct, enumはSendableを明記しなくても、暗黙的に準拠されます。(例外あり)

Swift
struct Person { // 暗黙的にSendableに準拠される
    let name: String // ✅ Sendable
    var age: Int     // ✅ Sendable(varだが問題ない)
}

以下はSendableではないプロパティを持っているので、Sendableには準拠できません。

Swift
class NonSendableClass {
    var value = 0
}

struct NonSendableStruct: Sendable {
    let value: NonSendableClass // ❌ Stored property 'value' of 'Sendable'-conforming struct 'NonSendableStruct' has non-sendable type 'NonSendableClass'
}

classの場合

classの場合、structよりも厳しい条件があります。

  • 全てのプロパティがSendableであること
  • 全てのプロパティがletであること
  • finalであること

以下はSendableに準拠できている例です。

classは常にSendableを明記してあげる必要があります。(自動的に補完されない)

Swift
final class Person: Sendable { // ✅ final & Sendable明記
    // ✅ 全てのプロパティがlet
    let name: String // ✅ Sendable
    let age: Int     // ✅ Sendable

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

クラス内でプロパティを変更したいなら、actorを使用するか、先述したMutexなどを使って、排他処理を自分で実装する必要があります。

まとめ

今回はSendableについて、導入された背景や使い方について解説しました。

Swift6では対応が必須になっていくので、今のうちに慣れておきましょう。

ぜひご自身の手で色々と試してみてください。

ここまでのご閲覧ありがとうございました!

参考にした記事

Sendable理解した - Qiita
はじめに Swift6が登場した現在のiOS開発において、Sendableというプロトコルを見かける頻度が爆増しました。 Sendable調べたこと何回かあるけど、毎回ふわっとしか理解せず次見かけた時「なんだっけこれ、、」と何度も調べる羽目...
Swift 6 - Sendable, @unchecked Sendable, @Sendable, sending and nonsending
Explore how Swift 6 uses Sendable, @Sendable, sending, and nonsending to enforce concurrency safety, ensure thread-safe ...

コメント

タイトルとURLをコピーしました