2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フューチャーAdvent Calendar 2024

Day 16

Strict Concurrency対応をActorによるmutable Stateを保護する事例で理解する

Last updated at Posted at 2024-12-15

本記事は、フューチャーアドベントカレンダー2024の16日目です。

はじめに

HealthCare Innovation Group(HIG)1所属の橋本です。

2024年9月16日にXcode 16がリリースされました!
Xcode16より内蔵されているSwift 6より、データ競合のチェックが厳しくなり、データ競合が起こる可能性のあるコードが含まれていると、コンパイル時にエラーとなってしまいます。

今回は、Actor用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて実際にどのような対応が必要になっていくるのかを見ていきます。

以下の記事もSwift Advent Calendar 2024の8日目としてフューチャー技術ブログに投稿していますので、Swift 6移行対応に興味のある方はぜひ、覗いてみてください。

環境

  • OS: macOS Sequoia 15.1.1(24B91)
  • Xcode: Version 16.2 (16C5032a)
  • Swift: 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)

本記事では、Swift6への移行準備する想定の記事となっており、以下の設定とする。

SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;

それぞれ、TARGETS > Build Settings から設定ができます。

  • SWIFT_STRICT_CONCURRENCY = completeの設定箇所

image.png

  • SWIFT_VERSION = 5.0の設定箇所

image.png

データ競合が起こる事例に対して、どのように解決していくか

今回は、WWDC2021の次のセッション内容をもとにデータ競合を防ぐ(=Strict Concurrecy対応)方法をみていきます。

データ競合が起こるときは、上記のセッションで次のように説明されています。

Data race occer when
・Two Thread concurrenclently access the same data
・One of them is a write

つまり、データ競合は

  • 同じデータに、2つのスレッドから平行にアクセスする
  • アクセスするときに、少なくとも1つが書き込みである

まずは、データ競合が起こる例を示す。

修正前のコード
class Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

func run() {
    let counter = Counter()

    Task.detached {
        print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
    }

    Task.detached {
        print(counter.increment()) // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode
    }
}

上記のようなコードを書くと、次のような警告メッセージがでます。

image.png

Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race; this is an error in the Swift 6 language mode

(日本語訳) 非Sendable型 @isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' の値が転送後にアクセスされました。後続のアクセスで競合が発生する可能性があります。これはSwift 6言語モードではエラーとなります。

このコードだと、「Swift 6モードへあげたとき、エラーになってしまいますよ」と教えてくれています。これがSWIFT_STRICT_CONCURRENCY = completeによる効果です。このように、Swift 5モードでSWIFT_STRICT_CONCURRENCY = completeとすることでSwift6移行に向けて、改善しないといけない箇所を洗い出すことができます。

Actorを用いてデータ競合を解決する

次のWWDC2021のセッション内容を参考にしています。

Actorとは

Actorは共有可変状態のための同期メカニズムです。すなわち、ActorActorインスタンスごとに残りのプログラムから隔離することで、データへの同期アクセスを実現しています。
Actorインスタンスごとに隔離することができる(Isolation domain)を持ち、隔離したIsolation domain内のデータにアクセスをするには、Actorを経由するため、並行にアクセスをされないことをActorが保証しています。

Actorは、Classと同様に参照型で、Swiftにおける型と同様の機能が提供されています。プロパティ、メソッド、イニシャライザ、サブスクリプトの機能を持ち、プロトコルに準拠させたり、エクステンションで拡張することもできます。

詳細は、次のProposal: SE-0306を確認してみてください。

Actorを使ってmutable Stateを保護する

さきほどのデータに競合が起こる可能性のあるコードをActorを用いて、改善していきます。

- class Counter {
+ actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

Counteractorとして、定義することでvalueプロパティへ並列にアクセスされることから保護することができます。これは、actorSendableに準拠しているため、並行タスクの間で安全に共有が可能になっています。

classactorに変えたことにより、次のエラーが発生します。

Expression is 'async' but is not marked with 'await'

これは、actor内のメソッドは非同期メソッドとして扱われるため、呼び出し時にawaitをつけずに呼び出しているためエラーとなっています。

func run() {
    let counter = Counter()

    Task.detached {
        print(counter.increment()) // Expression is 'async' but is not marked with 'await'
    }

    Task.detached {
        print(counter.increment()) // Expression is 'async' but is not marked with 'await'
    }
}

エラーメッセージの内容に従って、awaitをメソッドにつけます。

func run() {
    let counter = Counter()

    Task.detached {
-        print(counter.increment()) // エラー発生
+        print(await counter.increment()) 
    }

    Task.detached {
-        print(counter.increment()) // エラー発生
+        print(await counter.increment()) 
    }
}

以上で、Swift Concurrency Checkingの警告メッセージは消え、Swift 6でもエラーが出ない形に修正が完了しました。

修正後のコード全体
actor Counter {
    var value = 0

    func increment() -> Int {
        value = value + 1
        return value
    }
}

func run() {
    let counter = Counter()

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

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

さいごに

今回は、一つのActor用いてmutable Stateを保護する事例をとって、Swift 6移行に向けて、実際にどのように対応するかを振り返りました。

データ競合が起こり得る箇所は、コンパイル時に潰すことができるようになるのは最終的には意図しないクラッシュや不具合等の数を大きく減らすことができる期待できるため、ユーザーの離脱を減らすことにつながるため、ビジネス的にも大変有用だと考えています。

Swift 6移行に取り組む際の参考になれば幸いです。

  1. 医療・ヘルスケア分野での案件や新規ビジネス創出を担う、2020年に誕生した事業部です。設立エピソードは未来報の記事をご覧ください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?