【Swift Concurrency】Sendableプロトコルとは?スレッドセーフな型定義

【Swift Concurrency】Sendableプロトコルとは?スレッドセーフな型定義

この記事からわかること

  • Swift ConcurrencySendableプロトコルとは?
  • スレッドセーフ定義
  • クラスへの適応方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

公式リファレンス:Concurrency
公式ドキュメント:Concurrency

Swift Concurrencyとは?

Swift Concurrency(同時実効性)とはiOS15(Swift 5.5)から導入された仕組みの1つで非同期プログラミング(並列処理)をより利用しやすくするための機能を提供しています。asyncawaitキーワード、Task構造体などはSwift Concurrencyから提供されており、非同期処理を実装する際に利用されるcompletionHandler(コールバック関数)の弱みである可読性の低下を解消することができるようになりました。

Sendableプロトコルとは?

公式リファレンス:Sendableプロトコル

SendableプロトコルもSwift Concurrencyが提供する仕組みの一つ同時並行処理の中で異なるスレッド間にデータを安全に渡すためのルールを定義したものです。このプロトコルに準拠した型は複数のスレッドからアクセスしてもデータ競合が発生しないことが保証されます。

Sendableプロトコル自体には特に実装を要求するプロパティやメソッドなどは存在しません。このプロトコルを準拠した型が「データのやり取り(送受信)が安全に行える型」であることだけを保証します。

protocol Sendable { }

準拠させるための要件

Sendableプロトコルに準拠させるためには「データのやり取り(送受信)が安全に行える型」として定義する必要があります。プリミティブ型(IntStringBoolなど)の標準型はデフォルトでSendableに準拠しています。

独自で定義する場合はstructenumclassでは少し注意すべきポイントが異なります。

構造体や列挙型

構造体や列挙型でSendableに準拠させるためには以下のいずれかの条件を満たすようにします。条件を満たしている構造体や列挙型は暗黙的にSendableに準拠している状態になります。

// 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を準拠させるには以下の要件を満たす必要があります。

この要件満たす以下のようなクラスの場合は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

MyClassSendableではないがスレッドセーフであることがコードベースで保証されている場合は@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モードに順次移行するようにしないといけないですね。。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index