Sendableとは

Sendableとは、Swift 5.5で導入されたプロトコルで、並行処理において安全に異なるタスクや実行コンテキスト間でやり取りできる型を表すことができます。
簡潔にいうと、コンパイラに「この型は並行処理で安全に使えるよ!」「データ競合起こらないよ!」と伝える役割を持っています。
今回はSendableが導入された背景、使い方について自分なりに調べたことをまとめていきます。
動作環境
- macOS Tahoe 26.0
- Xcode 26.0 beta
データ競合
データ競合とは、並行処理において1つのリソースに複数のタスクが同時にアクセスして、結果が意図しないものになってしまうことです。
たとえば、以下のコードではCounterのインスタンスに、2つのTaskが同時にアクセスしてデータ競合がおきています。
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
Isolation domain(分離ドメイン)はデータ競合を防ぐために導入された概念です。
ここで分離されたタスクは1つずつ処理されて、データ競合は起こらないようになっています。
actor
や @MainActor
、 @globalActor
などを使って定義することができます。
先ほどのコードをactorを使って書き直してみましょう。
actor内のメソッドやプロパティにアクセスするには、awaitを使います。
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つずつ順番に処理されるので、データ競合が起こる心配はありません。

しかし、以下の場合はどうなるでしょうか?
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を使っています。
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を使って、以下のように書くこともできます。
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を明記しなくても、暗黙的に準拠されます。(例外あり)
struct Person { // 暗黙的にSendableに準拠される
let name: String // ✅ Sendable
var age: Int // ✅ Sendable(varだが問題ない)
}
以下はSendableではないプロパティを持っているので、Sendableには準拠できません。
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を明記してあげる必要があります。(自動的に補完されない)
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では対応が必須になっていくので、今のうちに慣れておきましょう。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
参考にした記事


コメント