3.5K Views
October 20, 24
スライド概要
2024 年 10 月 20 日に開催された「千歳ゆるい勉強会vol.4」で発表した Swift Concurrency の要所のお話です。
このスライドで伝えたいことは Swift Concurrency の本質1つで、そのほかの周辺情報は参考程度・学習の助けにしてもらえれば — みたいな気持ちで綴っています。各キーワードの説明はごく簡単に済ませてあって、わかりにくいところもあると思います。それらについては、どんなキーワードがあるのかを知るきっかけと、その漠然とした意味に触れておく感じで活用してもらえたら幸いです。
正統派趣味人プログラマー。プログラミングとは幼馴染です。
要諦 Swift Concurrency 熊谷友宏 @es̲kumagai 2024/10/20 千歳ゆるい勉強会 vol.4
Tomohiro Kumagai Swift 言語が好み これまでに使ったことのある言語 MSX-BASIC, Z80, R800, Forth, TLX, HTML, JavaScript, Perl, Java, Object Pascal, C++, VBScript, SQL, PHP, Fortran, Prolog, XML, Visual Basic, C#, Objective-C, Apple Script, Swift 小さな勉強会を 2014年9月27日より始めて今に至る 株式会社ゆめみさんで 熊谷さんのやさしい Swift 勉強会 を開催中 プログラミングの楽しさを伝えていきたい ⋯ ▶︎ ▶︎ ▶︎ ▶︎ 熊谷 友宏
Swift 5.5 に登場 → 6.0 で本格始動 Swift Concurrency ※ 今は Swift 6.0 が最新
並行処理を行うための仕組み Swift でも async / await が使えるようになった func exec() async -> Int { } この関数は非同期で実行する必要がある let value = await exec() この関数を非同期で実行し、実行が終わるまで待つ ⋯ ▶︎ ▶︎ Swift Concurrency とは
並行処理を行うための仕組み Swift でも async / await が使えるようになった func exec(_ arg: Int, callback: (_ result: Int) ) -> Void { callback(arg * 2) } これが ̶ func exec(_ arg: Int) async -> Int { return arg * 2 ̶ こうなる } ⋯ ▶︎ ▶︎ Swift Concurrency とは
並行処理を行うための仕組み Swift でも async / await が使えるようになった exec(10) { result in print(result) } これが ̶ await print(exec(10)) ̶ こうなる ⋯ ▶︎ ▶︎ Swift Concurrency とは
否 本質 は どうやら それでは ないらしい。
▶︎ ▶︎ ▶︎ Swift Concurrency とは 並行処理を安全に行うための仕組み 並行処理を Swift が把握して データ競合が起きる可能性を排除 async / await は その一環 func exec() async -> Int { } この関数は非同期で実行する必要がある let value = await exec() この関数を非同期で実行し、実行が終わるまで待つ
Swift Concurrency 要諦
▶︎ ▶︎ ▶︎ Swift Concurrency 本質 並行処理を安全に行う データ競合が起きる可能性のあるコードを検出し コンパイルエラーにして、データ競合を未然に防ぐ 競合状態の発生可能性を可視化する ※ 並行安全のコンパイラー支援が魅力
▶︎ ▶︎ Swift Concurrency データ競合とは 非同期処理で、予期しないデータになる現象 ひとつのメモリーを複数箇所で共有して、同時に参照、 そこに書込処理が伴うときに発生する ̶ ことがある ※ 再現性が低くなりがちで、デバッグを難しくする ※ これを未然に防ぐのが Swift Concurrency の存在意義
▶︎ ▶︎ Swift Concurrency 競合状態とは 非同期処理で、予期しないデータになる現象 一連の処理を実行中に別の処理が並行して走ることで、 意図した計算結果にならない ̶ ことがある ※ 再現性が低くなりがちで、デバッグを難しくする ※ これを可能な範囲で予防し、潜在箇所を視覚化するのが Swift Concurrency の存在意義
▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency データ競合の回避策 メモリーを共有しない イミュータブルクラス ロック シリアルキュー ロックフリー などなど ※ データ競合や競合状態は、マルチスレッドでは無視できない問題
▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency データ競合の回避策 メモリーを共有しない イミュータブルクラス ロック シリアルキュー ロックフリー などなど NEW Swift Concurrency ※ データ競合や競合状態は、マルチスレッドでは無視できない問題
▶︎ ▶︎ ▶︎ Swift Concurrency 導入された概念 タスク • 非同期処理の実行単位(タスク内の処理は同期的に実施) • 実行スレッドはシステムが管理(途中で変わることもある) 隔離領域 • データの保護単位(同一隔離領域内のタスクは同時実行されない) • 領域を超えての相互参照を制限し、データ競合を防ぐ 中断ポイント • 非同期処理の結果を待つポイント • スレッド自体はブロックしない
▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency 導入されたキーワード(抜粋) async Task @Sendable await actor @globalActor Actor GlobalActor Sendable nonisolated @MainActor isolated sending #isolation ※ これらを使い、並行安全が実施される箇所とその特色をコードに埋め込む
Swift Concurrency よく知られている機能
▶︎ よく知られている機能 1. async ① 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される この引数はタスクを跨ぐ この戻り値はタスクを跨ぐ func method(value: String) async -> Int { } この関数は非同期で実行される
▶︎ よく知られている機能 1. async ② 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される var property: String { get async { この値はタスクを跨ぐ } } この値は非同期で取得される ※ setter が async のときは、setter を併設できない
▶︎ よく知られている機能 1. async ③ 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される この変数の初期化式は非同期で実行される async let value = method() 最終的に、この値はタスクを跨ぐ ※ 初期化式が同期実行可能だとしても、非同期で実施
▶︎ よく知られている機能 2. await ① 付与した関数や変数を非同期で実行することを印付ける • 引数や戻り値は、タスクを跨いで受け渡しする • 非同期実行している間は処理を中断して待つ この戻り値はタスクを跨ぐ この引数はタスクを跨ぐ let result = await exec(value: "TEXT") この関数は非同期で実行する必要がある
▶︎ よく知られている機能 2. await ② 付与した関数や変数を非同期で実行することを印付ける • 引数や戻り値は、タスクを跨いで受け渡しする • 非同期実行している間は処理を中断して待つ この値はタスクを跨いで取得される await print(property) この変数の参照は非同期で実行する必要がある
Swift Concurrency 知っておきたい機能
▶︎ 知っておきたい機能 1. Task ① ブロック内が 非同期で実行されることを印付ける • タスクを新規に立ち上げて、呼び出しを実施 • キャプチャーした値やタスクからの戻り値は、タスクを跨ぐ このブロックを出入りする値はタスクを跨ぐ Task { } ブロック内の処理は非同期で実行 ※ 呼出元のタスク特性(隔離領域の継承、キャンセルの伝搬)を引き継ぐ
▶︎ 知っておきたい機能 1. Task ② ブロック内が 非同期で実行されることを印付ける • タスクを新規に立ち上げて、呼び出しを実施 • キャプチャーした値やタスクからの戻り値は、タスクを跨ぐ このブロックを出入りする値はタスクを跨ぐ Task.detached { } ブロック内の処理は非同期で実行 ※ 呼出元のタスク特性(隔離領域の継承、キャンセルの伝搬)を引き継がない
▶︎ 知っておきたい機能 2. actor ① インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { } 保有するプロパティーを、データ競合からインスタンス単位で保護
▶︎ 知っておきたい機能 2. actor ② インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { var state: Value } プロパティー編集時の参照と、外部からの直接変更を防ぐ
▶︎ 知っておきたい機能 2. actor ③ インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { func execution() -> Value } 実装コードを一連の処理として実行 & 実行中の処理があるときは待つ
▶︎ 知っておきたい機能 3. Actor ① アクターであることを印付けるプロトコル • すべてのアクター型が暗黙的に準拠 • これを継承したプロトコルが準拠できる型は、アクター型に限られる • アクターの動作を前提として扱える • 外部からのメソッド呼出やプロパティー参照は 非同期扱い protocol Something: Actor { } このプロトコルはアクターとして振る舞う
▶︎ 知っておきたい機能 3. Actor ② アクターであることを印付けるプロトコル • すべてのアクター型が暗黙的に準拠 • これを継承したプロトコルが準拠できる型は、アクター型に限られる • アクターの動作を前提として扱える • 外部からのメソッド呼出やプロパティー参照は 非同期扱い func something(_ instance: some Actor) { } 何らかのアクターを引数にとる
▶︎ 知っておきたい機能 4. Sendable ① 並行安全であることを印付けるプロトコル • 隔離領域を跨いで安全に受け渡し可能(スレッドセーフ) • 並行安全な型だけに適用可能(コンパイラーが検証) • コンパイラーが安全性を保証できないときでも強制的に適用可能 struct Something: Sendable { } この型は安全に隔離領域を跨げる
▶︎ 知っておきたい機能 4. Sendable ② 並行安全であることを印付けるプロトコル • 隔離領域を跨いで安全に受け渡し可能(スレッドセーフ) • 並行安全な型だけに適用可能(コンパイラーが検証) • コンパイラーが安全性を保証できないときでも強制的に適用可能 class Something: @unchecked Sendable { } この型は安全に隔離領域を跨げる ̶ と主張 ※ この場合、保証するのはプログラマーの役目
▶︎ 知っておきたい機能 5. nonisolated ① 非隔離であることを印付ける • 隔離領域による保護が行われない(同時実行される可能性あり) • 同期的に処理される • 通常の関数や変数は、暗黙的に nonisolated 扱い nonisolated func something() { } 隔離領域による保護を行わない
▶︎ 知っておきたい機能 5. nonisolated ② 非隔離であることを印付ける • 隔離領域による保護が行われない(同時実行される可能性あり) • 同期的に処理される • 通常の関数や変数は、暗黙的に nonisolated 扱い nonisolated(unsafe) var something: Value } 隔離領域による保護を行わない ※ 変数はデータ競合を起こす可能性があるため Unsafe 扱い
▶︎ 知っておきたい機能 6. @MainActor ① メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い @MainActor var something: Value メインアクター隔離で保護
▶︎ 知っておきたい機能 6. @MainActor ② メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い @MainActor func something() メインアクター隔離で保護
▶︎ 知っておきたい機能 6. @MainActor ③ メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い メインアクター隔離で保護 @MainActor class Something { } メンバーもメインアクター隔離で保護
▶︎ 知っておきたい機能 6. @MainActor ④ メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い メインアクター隔離で実行 Task { @MainActor in } 内部はメインアクター隔離で保護される
▶︎ ▶︎ ▶︎ 知っておきたい機能 ※ 中断ポイントと再入可能性 同一隔離領域のタスクは同時実行されない(非隔離領域を除く) await では処理が中断される 中断している間は、その隔離領域で別のタスクを実行可能 @MainActor class X { func method1() async { print("1-1") await something() print("1-2") } func method2() { print("2") } }
▶︎ ▶︎ ▶︎ 知っておきたい機能 ※ 中断ポイントと再入可能性 同一隔離領域のタスクは同時実行されない(非隔離領域を除く) await では処理が中断される 中断している間は、その隔離領域で別のタスクを実行可能 let x = X() Task { await x.method1() } Task { await x.method2() } // OUTPUT 1-1 2 1-2 中断時は他タスクが始動
Swift Concurrency そのうち知ればいい機能
▶︎ そのうち知ればいい機能 1. @Sendable ① 関数が並行安全であることを印付ける • 並行安全な要素で構成された関数に付与可能 • 隔離領域を跨いで安全に受け渡しと実行が可能 @Sendable func something(_ value: Int) -> Int { 隔離領域を跨いで安全に受け渡し可能 }
▶︎ そのうち知ればいい機能 1. @Sendable ② 関数が並行安全であることを印付ける • 並行安全な要素で構成された関数に付与可能 • 隔離領域を跨いで安全に受け渡しと実行が可能 let f: @Sendable (Int) -> Int { 隔離領域を跨いで安全に受け渡し可能 }
▶︎ そのうち知ればいい機能 2. @globalActor ① アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @globalActor actor Something { 大域隔離領域 Something を定義 }
▶︎ そのうち知ればいい機能 2. @globalActor ② アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @Something var something: Value 大域隔離領域 Something で保護
▶︎ そのうち知ればいい機能 2. @globalActor ③ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @Something func something() 大域隔離領域 Something で保護
▶︎ そのうち知ればいい機能 2. @globalActor ④ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない 大域隔離領域 Something で保護 @Something class Something { } メンバーも Something アクター隔離で保護
▶︎ そのうち知ればいい機能 2. @globalActor ⑤ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない 大域隔離領域 Something で保護 Task { @Something in } 内部は Something アクター隔離で保護される
▶︎ そのうち知ればいい機能 3. GlobalActor 大域アクターとして振る舞えることを印付けるプロトコル • すべての大域アクター型が暗黙的に準拠する様子 • これを継承したプロトコルが準拠できる型は、大域アクター型に限られる • 自身がアクターであることは求めない様子(唯一なアクターさえ示せれば良い) • 拡張したメソッド呼出やプロパティー参照は、同期扱いになる様子 protocol Something: GlobalActor { } このプロトコルは唯一のアクターを提供できる ※ 自身がアクターとして振る舞うのではなく、唯一の隔離領域を提供する役目を担う
Swift Concurrency ひとまず知らなくてもいい機能
▶︎ ひとまず知らなくてもいい機能 1. isolated 関数が指定した隔離領域下で動作することを印付ける • 指定インスタンスを隔離領域として関数を実行 • 指定の隔離下からは同期実行、その外からは非同期実行 • 指定の隔離領域で保護されたプロパティーに直接アクセスが可能 func something(on actor: isolated SomeActor) -> Int { } 実装コードは、指定した隔離領域内で実行される
▶︎ ひとまず知らなくてもいい機能 2. sending ① 隔離領域を超えて受け渡すことを印付ける • 引数や戻り値の型に指定できる • 対応する値は、Sendable でなくても隔離領域を跨げる • コンパイラーは、同時アクセスされないための検査を実施 func something(_ value: sending Value) let value = Value() something(value) 隔離領域を跨いで扱うことを主張している 呼出先で隔離領域を跨ぐが、以降で使っていないなら渡せる
▶︎ ひとまず知らなくてもいい機能 2. sending ② 隔離領域を超えて受け渡すことを印付ける • 引数や戻り値の型に指定できる • 対応する値は、Sendable でなくても隔離領域を跨げる • コンパイラーは、同時アクセスされないための検査を実施 func something() async -> sending Value 戻り値が隔離領域を跨げることを主張 @MainActor func action() { let value = await something() } 別の隔離領域で戻り値を受け取れる
▶︎ ひとまず知らなくてもいい機能 3. #isolation ① 現在の隔離領域を取得するマクロ • 現在の隔離領域をインスタンスで取得可能 • 隔離領域に属さないときは nil になる • 実行する隔離領域の指定に使える @MainActor func action() { 現在の隔離領域を取得 print(#isolation, assert(#isolation === MainActor.shared) } 同一性演算子での比較も可能
▶︎ ひとまず知らなくてもいい機能 3. #isolation ② 現在の隔離領域を取得するマクロ • 現在の隔離領域をインスタンスで取得可能 • 隔離領域に属さないときは nil になる • 実行する隔離領域の指定に使える 省略時は現在の隔離領域を引き継ぐ func action(on actor: isolated Actor? = #isolation) { } 実装コードは、受け取ったアクターの保護化におかれる
まとめ
▶︎ ▶︎ Swift Concurrency まとめ Swift Concurrency の本質 データ競合を起こさせないためにのみ存在する 思いのほか、追加機能はたくさんある これらでコードに印を付けて、データ競合が起こり得るところを Swift に検出してもらうのが Swift Concurrency の醍醐味
Enjoy! Swift Thank you 熊谷友宏 @es̲kumagai