Swift Concurrency 要諦 #ゆるちとせ

640 Views

October 20, 24

スライド概要

2024 年 10 月 20 日に開催された「千歳ゆるい勉強会vol.4」で発表した Swift Concurrency の要所のお話です。

profile-image

正統派趣味人プログラマー。プログラミングとは幼馴染です。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

要諦 Swift Concurrency 熊谷友宏 @es̲kumagai 2024/10/20 千歳ゆるい勉強会 vol.4

2.

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 勉強会 を開催中 プログラミングの楽しさを伝えていきたい ⋯ ▶︎ ▶︎ ▶︎ ▶︎ 熊谷 友宏

3.

Swift 5.5 に登場 → 6.0 で本格始動 Swift Concurrency ※ 今は Swift 6.0 が最新

4.

並行処理を行うための仕組み Swift でも async / await が使えるようになった func exec() async -> Int { } この関数は非同期で実行する必要がある let value = await exec() この関数を非同期で実行し、実行が終わるまで待つ ⋯ ▶︎ ▶︎ Swift Concurrency とは

5.

並行処理を行うための仕組み 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 とは

6.

並行処理を行うための仕組み Swift でも async / await が使えるようになった exec(10) { result in print(result) } これが ̶ await print(exec(10)) ̶ こうなる ⋯ ▶︎ ▶︎ Swift Concurrency とは

7.

否 本質 は どうやら それでは ないらしい。

8.

▶︎ ▶︎ ▶︎ Swift Concurrency とは 並行処理を安全に行うための仕組み 並行処理を Swift が把握して データ競合が起きる可能性を排除 async / await は その一環 func exec() async -> Int { } この関数は非同期で実行する必要がある let value = await exec() この関数を非同期で実行し、実行が終わるまで待つ

9.

Swift Concurrency 要諦

10.

▶︎ ▶︎ ▶︎ Swift Concurrency 本質 並行処理を安全に行う データ競合が起きる可能性のあるコードを検出し コンパイルエラーにして、データ競合を未然に防ぐ 競合状態の発生可能性を可視化する ※ 並行安全のコンパイラー支援が魅力

11.

▶︎ ▶︎ Swift Concurrency データ競合とは 非同期処理で、予期しないデータになる現象 ひとつのメモリーを複数箇所で共有して、同時に参照、 そこに書込処理が伴うときに発生する ̶ ことがある ※ 再現性が低くなりがちで、デバッグを難しくする ※ これを未然に防ぐのが Swift Concurrency の存在意義

12.

▶︎ ▶︎ Swift Concurrency 競合状態とは 非同期処理で、予期しないデータになる現象 一連の処理を実行中に別の処理が並行して走ることで、 意図した計算結果にならない ̶ ことがある ※ 再現性が低くなりがちで、デバッグを難しくする ※ これを可能な範囲で予防し、潜在箇所を視覚化するのが Swift Concurrency の存在意義

13.

▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency データ競合の回避策 メモリーを共有しない イミュータブルクラス ロック シリアルキュー ロックフリー などなど ※ データ競合や競合状態は、マルチスレッドでは無視できない問題

14.

▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency データ競合の回避策 メモリーを共有しない イミュータブルクラス ロック シリアルキュー ロックフリー などなど NEW Swift Concurrency ※ データ競合や競合状態は、マルチスレッドでは無視できない問題

15.

▶︎ ▶︎ ▶︎ Swift Concurrency 導入された概念 タスク • 非同期処理の実行単位(タスク内の処理は同期的に実施) • 実行スレッドはシステムが管理(途中で変わることもある) 隔離領域 • データの保護単位(同一隔離領域内のタスクは同時実行されない) • 領域を超えての相互参照を制限し、データ競合を防ぐ 中断ポイント • 非同期処理の結果を待つポイント • スレッド自体はブロックしない

16.

▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ ▶︎ Swift Concurrency 導入されたキーワード(抜粋) async Task @Sendable await actor @globalActor Actor GlobalActor Sendable nonisolated @MainActor isolated sending #isolation ※ これらを使い、並行安全が実施される箇所とその特色をコードに埋め込む

17.

Swift Concurrency よく知られている機能

18.

▶︎ よく知られている機能 1. async ① 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される この引数はタスクを跨ぐ この戻り値はタスクを跨ぐ func method(value: String) async -> Int { } この関数は非同期で実行される

19.

▶︎ よく知られている機能 1. async ② 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される var property: String { get async { この値はタスクを跨ぐ } } この値は非同期で取得される ※ setter が async のときは、setter を併設できない

20.

▶︎ よく知られている機能 1. async ③ 付与した関数や変数が 非同期で実行されることを印付ける • 非同期での呼び出しを要求 • 引数や戻り値は、タスクを跨いで受け渡される この変数の初期化式は非同期で実行される async let value = method() 最終的に、この値はタスクを跨ぐ ※ 初期化式が同期実行可能だとしても、非同期で実施

21.

▶︎ よく知られている機能 2. await ① 付与した関数や変数を非同期で実行することを印付ける • 引数や戻り値は、タスクを跨いで受け渡しする • 非同期実行している間は処理を中断して待つ この戻り値はタスクを跨ぐ この引数はタスクを跨ぐ let result = await exec(value: "TEXT") この関数は非同期で実行する必要がある

22.

▶︎ よく知られている機能 2. await ② 付与した関数や変数を非同期で実行することを印付ける • 引数や戻り値は、タスクを跨いで受け渡しする • 非同期実行している間は処理を中断して待つ この値はタスクを跨いで取得される await print(property) この変数の参照は非同期で実行する必要がある

23.

Swift Concurrency 知っておきたい機能

24.

▶︎ 知っておきたい機能 1. Task ① ブロック内が 非同期で実行されることを印付ける • タスクを新規に立ち上げて、呼び出しを実施 • キャプチャーした値やタスクからの戻り値は、タスクを跨ぐ このブロックを出入りする値はタスクを跨ぐ Task { } ブロック内の処理は非同期で実行 ※ 呼出元のタスク特性(隔離領域の継承、キャンセルの伝搬)を引き継ぐ

25.

▶︎ 知っておきたい機能 1. Task ② ブロック内が 非同期で実行されることを印付ける • タスクを新規に立ち上げて、呼び出しを実施 • キャプチャーした値やタスクからの戻り値は、タスクを跨ぐ このブロックを出入りする値はタスクを跨ぐ Task.detached { } ブロック内の処理は非同期で実行 ※ 呼出元のタスク特性(隔離領域の継承、キャンセルの伝搬)を引き継がない

26.

▶︎ 知っておきたい機能 2. actor ① インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { } 保有するプロパティーを、データ競合からインスタンス単位で保護

27.

▶︎ 知っておきたい機能 2. actor ② インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { var state: Value } プロパティー編集時の参照と、外部からの直接変更を防ぐ

28.

▶︎ 知っておきたい機能 2. actor ③ インスタンスが隔離領域をつくることを印付ける型 • プロパティーをデータ競合から保護 • メソッドを競合状態から保護 • インスタンス単位で隔離領域を作り、並行処理を阻止 • 外部からのメソッド呼出やプロパティー参照は 非同期扱い actor Something { func execution() -> Value } 実装コードを一連の処理として実行 & 実行中の処理があるときは待つ

29.

▶︎ 知っておきたい機能 3. Actor ① アクターであることを印付けるプロトコル • すべてのアクター型が暗黙的に準拠 • これを継承したプロトコルが準拠できる型は、アクター型に限られる • アクターの動作を前提として扱える • 外部からのメソッド呼出やプロパティー参照は 非同期扱い protocol Something: Actor { } このプロトコルはアクターとして振る舞う

30.

▶︎ 知っておきたい機能 3. Actor ② アクターであることを印付けるプロトコル • すべてのアクター型が暗黙的に準拠 • これを継承したプロトコルが準拠できる型は、アクター型に限られる • アクターの動作を前提として扱える • 外部からのメソッド呼出やプロパティー参照は 非同期扱い func something(_ instance: some Actor) { } 何らかのアクターを引数にとる

31.

▶︎ 知っておきたい機能 4. Sendable ① 並行安全であることを印付けるプロトコル • 隔離領域を跨いで安全に受け渡し可能(スレッドセーフ) • 並行安全な型だけに適用可能(コンパイラーが検証) • コンパイラーが安全性を保証できないときでも強制的に適用可能 struct Something: Sendable { } この型は安全に隔離領域を跨げる

32.

▶︎ 知っておきたい機能 4. Sendable ② 並行安全であることを印付けるプロトコル • 隔離領域を跨いで安全に受け渡し可能(スレッドセーフ) • 並行安全な型だけに適用可能(コンパイラーが検証) • コンパイラーが安全性を保証できないときでも強制的に適用可能 class Something: @unchecked Sendable { } この型は安全に隔離領域を跨げる ̶ と主張 ※ この場合、保証するのはプログラマーの役目

33.

▶︎ 知っておきたい機能 5. nonisolated ① 非隔離であることを印付ける • 隔離領域による保護が行われない(同時実行される可能性あり) • 同期的に処理される • 通常の関数や変数は、暗黙的に nonisolated 扱い nonisolated func something() { } 隔離領域による保護を行わない

34.

▶︎ 知っておきたい機能 5. nonisolated ② 非隔離であることを印付ける • 隔離領域による保護が行われない(同時実行される可能性あり) • 同期的に処理される • 通常の関数や変数は、暗黙的に nonisolated 扱い nonisolated(unsafe) var something: Value } 隔離領域による保護を行わない ※ 変数はデータ競合を起こす可能性があるため Unsafe 扱い

35.

▶︎ 知っておきたい機能 6. @MainActor ① メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い @MainActor var something: Value メインアクター隔離で保護

36.

▶︎ 知っておきたい機能 6. @MainActor ② メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い @MainActor func something() メインアクター隔離で保護

37.

▶︎ 知っておきたい機能 6. @MainActor ③ メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い メインアクター隔離で保護 @MainActor class Something { } メンバーもメインアクター隔離で保護

38.

▶︎ 知っておきたい機能 6. @MainActor ④ メインアクター隔離であることを印付ける • いわゆるメインスレッドで処理される • プロパティー、関数、メソッド、型、タスクなどで指定可能 • 通常は非同期扱い、メインアクター隔離内では同期扱い メインアクター隔離で実行 Task { @MainActor in } 内部はメインアクター隔離で保護される

39.

▶︎ ▶︎ ▶︎ 知っておきたい機能 ※ 中断ポイントと再入可能性 同一隔離領域のタスクは同時実行されない(非隔離領域を除く) await では処理が中断される 中断している間は、その隔離領域で別のタスクを実行可能 @MainActor class X { func method1() async { print("1-1") await something() print("1-2") } func method2() { print("2") } }

40.

▶︎ ▶︎ ▶︎ 知っておきたい機能 ※ 中断ポイントと再入可能性 同一隔離領域のタスクは同時実行されない(非隔離領域を除く) await では処理が中断される 中断している間は、その隔離領域で別のタスクを実行可能 let x = X() Task { await x.method1() } Task { await x.method2() } // OUTPUT 1-1 2 1-2 中断時は他タスクが始動

41.

Swift Concurrency そのうち知ればいい機能

42.

▶︎ そのうち知ればいい機能 1. @Sendable ① 関数が並行安全であることを印付ける • 並行安全な要素で構成された関数に付与可能 • 隔離領域を跨いで安全に受け渡しと実行が可能 @Sendable func something(_ value: Int) -> Int { 隔離領域を跨いで安全に受け渡し可能 }

43.

▶︎ そのうち知ればいい機能 1. @Sendable ② 関数が並行安全であることを印付ける • 並行安全な要素で構成された関数に付与可能 • 隔離領域を跨いで安全に受け渡しと実行が可能 let f: @Sendable (Int) -> Int { 隔離領域を跨いで安全に受け渡し可能 }

44.

▶︎ そのうち知ればいい機能 2. @globalActor ① アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @globalActor actor Something { 大域隔離領域 Something を定義 }

45.

▶︎ そのうち知ればいい機能 2. @globalActor ② アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @Something var something: Value 大域隔離領域 Something で保護

46.

▶︎ そのうち知ればいい機能 2. @globalActor ③ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない @Something func something() 大域隔離領域 Something で保護

47.

▶︎ そのうち知ればいい機能 2. @globalActor ④ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない 大域隔離領域 Something で保護 @Something class Something { } メンバーも Something アクター隔離で保護

48.

▶︎ そのうち知ればいい機能 2. @globalActor ⑤ アクターが大域的な隔離領域であることを印付ける • 型単位の隔離領域(通常のアクターはインスタンス単位) • メインスレッドも大域隔離領域のひとつ(@大域隔離領域名で扱う) • 通常は非同期扱い、自身の隔離内では同期扱い • 同じ大域隔離領域に属するタスクは同時実行されない 大域隔離領域 Something で保護 Task { @Something in } 内部は Something アクター隔離で保護される

49.

▶︎ そのうち知ればいい機能 3. GlobalActor 大域アクターとして振る舞えることを印付けるプロトコル • すべての大域アクター型が暗黙的に準拠する様子 • これを継承したプロトコルが準拠できる型は、大域アクター型に限られる • 自身がアクターであることは求めない様子(唯一なアクターさえ示せれば良い) • 拡張したメソッド呼出やプロパティー参照は、同期扱いになる様子 protocol Something: GlobalActor { } このプロトコルは唯一のアクターを提供できる ※ 自身がアクターとして振る舞うのではなく、唯一の隔離領域を提供する役目を担う

50.

Swift Concurrency ひとまず知らなくてもいい機能

51.

▶︎ ひとまず知らなくてもいい機能 1. isolated 関数が指定した隔離領域下で動作することを印付ける • 指定インスタンスを隔離領域として関数を実行 • 指定の隔離下からは同期実行、その外からは非同期実行 • 指定の隔離領域で保護されたプロパティーに直接アクセスが可能 func something(on actor: isolated SomeActor) -> Int { } 実装コードは、指定した隔離領域内で実行される

52.

▶︎ ひとまず知らなくてもいい機能 2. sending ① 隔離領域を超えて受け渡すことを印付ける • 引数や戻り値の型に指定できる • 対応する値は、Sendable でなくても隔離領域を跨げる • コンパイラーは、同時アクセスされないための検査を実施 func something(_ value: sending Value) let value = Value() something(value) 隔離領域を跨いで扱うことを主張している 呼出先で隔離領域を跨ぐが、以降で使っていないなら渡せる

53.

▶︎ ひとまず知らなくてもいい機能 2. sending ② 隔離領域を超えて受け渡すことを印付ける • 引数や戻り値の型に指定できる • 対応する値は、Sendable でなくても隔離領域を跨げる • コンパイラーは、同時アクセスされないための検査を実施 func something() async -> sending Value 戻り値が隔離領域を跨げることを主張 @MainActor func action() { let value = await something() } 別の隔離領域で戻り値を受け取れる

54.

▶︎ ひとまず知らなくてもいい機能 3. #isolation ① 現在の隔離領域を取得するマクロ • 現在の隔離領域をインスタンスで取得可能 • 隔離領域に属さないときは nil になる • 実行する隔離領域の指定に使える @MainActor func action() { 現在の隔離領域を取得 print(#isolation, assert(#isolation === MainActor.shared) } 同一性演算子での比較も可能

55.

▶︎ ひとまず知らなくてもいい機能 3. #isolation ② 現在の隔離領域を取得するマクロ • 現在の隔離領域をインスタンスで取得可能 • 隔離領域に属さないときは nil になる • 実行する隔離領域の指定に使える 省略時は現在の隔離領域を引き継ぐ func action(on actor: isolated Actor? = #isolation) { } 実装コードは、受け取ったアクターの保護化におかれる

56.

まとめ

57.

▶︎ ▶︎ Swift Concurrency まとめ Swift Concurrency の本質 データ競合を起こさせないためにのみ存在する 思いのほか、追加機能はたくさんある これらでコードに印を付けて、データ競合が起こり得るところを Swift に検出してもらうのが Swift Concurrency の醍醐味

58.

Enjoy! Swift Thank you 熊谷友宏 @es̲kumagai