【Swift Concurrency】Sendableプロトコルとは?スレッドセーフな型定義
この記事からわかること
- Swift ConcurrencyのSendableのプロトコルとは?
- スレッドセーフな型で定義
- クラスへの適応方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- macOS:Sonoma 14.6.1
公式リファレンス:Concurrency
公式ドキュメント:Concurrency
Swift Concurrencyとは?
Swift Concurrency(同時実効性)とはiOS15(Swift 5.5)から導入された仕組みの1つで非同期プログラミング(並列処理)をより利用しやすくするための機能を提供しています。async
やawait
キーワード、Task
構造体などはSwift Concurrencyから提供されており、非同期処理を実装する際に利用されるcompletionHandler(コールバック関数)の弱みである可読性の低下を解消することができるようになりました。
Sendableプロトコルとは?
Sendable
プロトコルもSwift Concurrencyが提供する仕組みの一つで同時並行処理の中で異なるスレッド間にデータを安全に渡すためのルールを定義したものです。このプロトコルに準拠した型は複数のスレッドからアクセスしてもデータ競合が発生しないことが保証されます。
Sendable
プロトコル自体には特に実装を要求するプロパティやメソッドなどは存在しません。このプロトコルを準拠した型が「データのやり取り(送受信)が安全に行える型」であることだけを保証します。
protocol Sendable { }
準拠させるための要件
Sendable
プロトコルに準拠させるためには「データのやり取り(送受信)が安全に行える型」として定義する必要があります。プリミティブ型(Int
、String
、Bool
など)の標準型はデフォルトでSendable
に準拠しています。
独自で定義する場合はstruct
やenum
とclass
では少し注意すべきポイントが異なります。
構造体や列挙型
構造体や列挙型でSendable
に準拠させるためには以下のいずれかの条件を満たすようにします。条件を満たしている構造体や列挙型は暗黙的にSendable
に準拠している状態になります。
- 全てのプロパティがlet
- 全てのプロパティがSendableに準拠
- var宣言でもスレッド間で共有されたときにデータ競合が発生しないこと
// MyStructも暗黙的にSendableになる
struct MyStruct {
let id: Int // IntはSendable
let name: String // StringもSendable
}
var
で定義した変数であっても構造体は値渡しであるためスレッド間で共有されても競合が発生しないため問題なくSendable
に準拠します。
struct MyStruct: Sendable {
var counter: Int
}
クラス
おすすめ記事:クラスと構造体の違い 参照渡しと値渡し
クラスは参照渡しであるためスレッド間で競合が発生しやすい型になります。そのため基本的にはSendable
に準拠しません。
// 非Sendable
class MyClass { }
非Sendable
なクラスを構造体のプロパティに定義してSendable
を明示的に記述するとStored property 'data' of 'Sendable'-conforming struct 'MyStruct' has non-sendable type 'MyClass'
という警告が発生します。これはSwift6モードにするとエラーとしてコンパイルできなくなります。
struct MyStruct: Sendable {
// Stored property 'data' of 'Sendable'-conforming struct 'MyStruct' has non-sendable type 'MyClass'; this is an error in the Swift 6 language mode
var data: MyClass
}
クラスをSendableに準拠させる方法
クラスにSendable
を準拠させるには以下の要件を満たす必要があります。
- final classである
- let定義の不変なプロパティかつSendableに準拠
- スーパークラスを持たないか、NSObjectをスーパークラスとして持つ
この要件満たす以下のようなクラスの場合はSendable
を準拠させることが可能です。
final class MyClass: NSObject, Sendable {
let value: Int
let message: String
init(value: Int, message: String) {
self.value = value
self.message = message
}
}
@unchecked Sendable
MyClass
がSendable
ではないがスレッドセーフであることがコードベースで保証されている場合は@unchecked Sendable
とすることでコンパイル時の要件チェックを無視することができます。開発者側がスレッドセーフな設計を行うことに委ねられるのでNSLock
などを使用して明示的な排他制御を実装する必要があります。
struct MyStruct: @unchecked Sendable {
var data: MyClass
}
class MyClass {
// このクラスがスレッドセーフであることを設計上保証する
private let lock = NSLock()
private var _value: Int = 0
func updateValue(newValue: Int) {
lock.lock()
defer { lock.unlock() }
_value = newValue
}
func getValue() -> Int {
lock.lock()
defer { lock.unlock() }
return _value
}
}
アクター
Actor
はSwift Concurrencyから提供されている機能の1つで、データの排他制御を行うことができるスレッドセーフなオブジェクトです。Actor自体がスレッドセーフな型のためデフォルトで暗黙的にSendable
に準拠しています。
Swift6からコンパイル時に検出できるように
Xcode16からSwift6モードのコンパイラが搭載されるようになりその中には「Strict concurrency checking(厳密な並行性チェック)」があります。特定の再現手順の確立が容易でない場合もあるため、デバッグが困難であったデータ競合の可能性をコンパイル時に検出することが可能になりました。
Swift6モードを有効にすることでデータの競合がチェックされるようになりSendable
をより明示的に宣言することが必要になります。データの競合が起こりうる箇所に警告やエラーが表示されるようになるのでactor
に変換したりSendable
を準拠させてその型がスレッドセーフであることをコードベースで記述していく必要があります。
Sendable
の役割を理解しつつSwift6モードに順次移行するようにしないといけないですね。。
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。