ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
構造化並行処理の基本を超えて
タスクツリーの重要性について確認し、構造化並行処理がアプリでの自動タスクキャンセル、タスク優先度のプロパゲート、有用なタスクローカル値パターンの管理にどのように役立つのか学びましょう。便利なパターンや最新のタスクグループAPIを使用してアプリ内のリソースを管理する方法を紹介します。タスクツリーやタスクローカル値のパワーを活用して分散システムに対するインサイトを共有します。視聴される前に、WWDC21の「Swift Concurrency: Behind the scenes」と「Explore structured concurrency in Swift」で「Explore structured concurrency in Swift」をご確認ください。
関連する章
- 0:56 - Structured concurrency
- 3:11 - Task tree
- 3:44 - Task cancellation
- 5:26 - withTaskCancellationHandler
- 8:36 - Task priority
- 10:23 - Patterns with task groups
- 11:27 - Limiting concurrent tasks in TaskGroups
- 12:22 - DiscardingTaskGroup
- 13:53 - Task-local values
- 16:58 - swift-log
- 17:19 - MetadataProvider
- 18:58 - Task traces
- 19:46 - Swift-Distributed-Tracing
- 20:42 - Instrumenting distributed computations
- 23:38 - Wrap-up
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
-
ダウンロード
♪音楽♪
こんにちは Evanです 今日は 構造化並行処理の応用として 構造化タスクがどのように便利な動作を シンプルに実現するかについて探ります まず構造化並行処理を復習するには WWDC2021の 「Explore structured concurrency in Swift」と 「Swift concurrency: Behind the scenes」をご覧ください 今日はタスク階層構造を見直し 自動的なタスクのキャンセルや優先処理 便利な値の動作を可能にする 方法にを見ていきます リソース使用量を管理するため タスクグループの いくつかのパターンについて説明します 最後にこれらすべてを総合して サーバ環境でのタスクの プロファイルとトレースを 容易にする方法を見ます 構造化並行処理は実行が分岐し 並行して実行され その実行結果が再結合する 明確に定義されるポイントを使い 並行コードについての 理解を容易にします これは ifブロック や forループが 同期コードで制御フローの 振る舞いを定義するのと似ています 並行実行は async letや タスクグループを使用したり タスクやデタッチドタスクを 作成する際に トリガーされます 実行結果は await の サスペンションポイントで 現在の実行に再結合します すべてのタスクが構造化する わけではありません 構造化タスクは「async let」と タスクグループを使って作成され 非構造化タスクはTask及び Task.detachedを使い作成されます 構造化タスクは ローカル変数の様に 宣言されたスコープの 終わりまで生存し スコープを抜けると 自動的にキャンセルされるため タスク寿命が明確になります 可能な限り構造化タスクを 使うことをお勧めします 後ほど説明する 構造化並行処理の利点は 常に非構造化タスクに 適用されるわけではありません コードに入る前に 具体例を考えてみましょう 複数の調理スタッフがスープを準備する キッチンがあるとします スープの準備には複数の ステップがあります 調理スタッフは食事をテーブルに出す前に 材料を切ったり 鶏肉の下準備やスープの沸騰をしたり する必要があります 一部のタスクは並行して 実行できますが 他のタスクは特定の順序で 実行する必要があります これをコードで表現する 方法を見ましょう まずは makeSoup関数に 焦点を当てます 関数に並行性を追加するため 非構造化タスクを作成し 必要に応じてそれらの値を 待機する場合があります これは並行実行可能なタスクと そうでないタスクを表現しますが これは推奨されるSwiftの並行処理の 利用方法ではありません 同じ関数を構造化並行処理を 使えば次のようになります 既知の数の子タスクを 作成するため 便利な async let 構文を 使用できます これらのタスクは親タスクと 構造的な関係を形成します なぜこれが重要なのかを説明します makeSoup は幾つかの 非同期関数を呼び出します その1つがchopIngredientsで 材料のリストを受取り タスクグループを使い 材料を並行して切る関数です makeSoupに慣れたので タスク階層を見ましょう 子タスクは色付きの ボックスで示され 矢印は親タスクから 子タスクへ向かっています makeSoupには 材料の切り方 鶏肉の漬け込み スープの沸騰という 3つの子タスクがあります chopIngredientsは 各材料の子タスクを作成します もし材料3つあれば さらに子タスクが3つ作成されます この親子の階層は タスクツリーと呼ばれます タスクツリーを紹介した所で どのようにコードに 利益をもたらすか確認しましょう タスクキャンセルはアプリが そのタスクの結果を不要とし タスクが停止して 部分的な結果を返すか エラーをスローすることを 示すために使用されます スープの例では 顧客が去ったり 別のものを注文したいと決めたり あるいは閉店時間である場合など 注文を中止したい場合が想定されます タスクのキャンセルの原因は 何でしょうか 構造化タスクはスコープを超えると 暗黙的にキャンセルされますが タスクグループで cancelAll を呼出すことでアクティブな 子タスクをすべてキャンセルできます 非構造化のタスクはcancel関数を使って 明示的にキャンセルされます 親タスクをキャンセルすると属する すべての子タスクもキャンセルされます 子タスクは即時に停止される わけではありません 単にタスクの isCancelled フラグが設定されるだけです 実際のキャンセル処理は コード内で行われます キャンセルは競合状態です もしチェック前にキャンセルすると makeSoupは SoupCancellationError をスローします ガードが実行された後に タスクがキャンセルされたら プログラムはスープの準備を続けます 部分的なキャンセルエラーを スローする代わりに Task.checkCancellationを 呼出すことで タスクがキャンセルされた場合 CancellationErrorをスローできます 結果がまだ必要かどうかを 確認するための 高コストな作業を開始する前に タスクのキャンセルのステータスを チェックすることが重要です 非同期または同期の関数であっても キャンセルに応答する必要がある場合は 続行前にキャンセルのステータスを 確認する必要があります isCancelled や checkCancellation で キャンセルを ポーリングすることは タスクが実行中時には有用ですが タスクが中断していて コードが実行してない間に キャンセルに 応答する必要があることもあります 例えばAsyncSequenceを 実装する場合などです withTaskCancellationHandler が ここで役立ちます shift関数を紹介します 調理スタッフはタスクキャンセルによる シフト終了のシグナルが出るまで 注文が入るたびにスープを 作る必要があります キャンセルのシナリオの1つで 非同期のforループが キャンセルされる前に 新しい注文が入る場合です makeSoup関数は 先ほど定義したように キャンセルを処理し エラーをスローします 別のシナリオはタスクが 中断され次の注文を待つ間に キャンセルが 発生する可能性があります タスク実行されていないので キャンセルイベントを 明示的にポーリングできません 代替でキャンセルハンドラで キャンセルイベントを検出し 非同期のforループから 抜け出す必要があります 注文は AsyncSequenceから生成されます これはAsyncIteratorにより駆動され 非同期のnext関数を定義します 同期的なイテレータと同様に next関数はシーケンスの次の要素を返すか シーケンスの終わりを示す nilを返します 多くのAsyncSequenceは シーケンスを停止するために ステートマシンを使用して 実装されています この例では isRunning が true の場合 シーケンスは注文の発行を 続ける必要があります タスクがキャンセルされると シーケンスが完了し シャットダウンすることを 示す必要があります これを行うために シーケンスのステートマシンに 同期的に cancel関数を呼び出します キャンセルハンドラが即座に 実行されるため ステートマシンはキャンセルハンドラと メインボディの間で 変更可能な状態で並行して 実行されます ステートマシンを保護する 必要があります アクターはカプセル化された状態を 保護する上で優れていますが ステートマシンの個々のプロパティを 変更し読み取る必要があるため アクターは適切なツールではありません さらにアクターでは操作の 実行順序を保証できないため 最初にキャンセルが実行する ことを保証できません そのため別の方法が必要です Swift Atomics のAtomic操作を 使うことにしましたが ディスパッチキューまたは ロックを使用できます これらのメカニズムを使うと 共有されたステートを同期させ 競合状態を避けつつ 非構造化のタスクを キャンセルハンドラに導入することなく 実行中のステートマシンを キャンセルできます タスクツリーはその情報を 自動的にプロパゲートします キャンセルトークンや同期の 心配をする必要はなく Swiftランタイムが安全に 処理してくれます キャンセルはタスクの実行を 停止させるのではなく タスクにキャンセルされた ことを通知し できるだけ早く 実行を停止するための シグナルです キャンセルのチェックは コードに任されます 次に構造化タスクツリーが どう優先度をプロパゲートし 優先度逆転を回避するのに 役立つかを考えましょう 優先度とは何か そもそもなぜ重要なのでしょうか 優先度はタスクがどれだけ緊急であるかを システムに伝える方法です ボタンの押下への応答のような 特定タスクでは 即座に実行しないとアプリが フリーズしたように見えます 一方サーバからコンテンツを プリフェッチするようなタスクであれば 誰も気づかない間にバックグラウンドで 実行できます 次に 優先度逆転とは何か 優先度逆転は 高優先度の タスクが低優先度の タスクの結果を待っている状況です デフォルトでは子タスクは 親から優先度を継承するため makeSoupが中間優先度で 実行されていると仮定すると すべての子タスクも 中間優先度で実行されます 例えばレストランにスープを 注文しに来たVIP客がいます 客から良い評価を得るために より高い優先度を設定します スープを待つ間 子タスク 優先度がエスカレートされ 高優先度のタスクが 低優先度のタスクを 待たないことが保証され 優先度逆転が回避されます より高い優先度のタスクの 結果を待つことで 子タスクの優先度が エスカレートします タスクグループの 結果を待つことは 次に完了するタスクが どれか分からないので すべての子タスクの優先度を エスカレートさせます 並行ランタイムは 優先度キューで予約し より高優先度のタスクを 優先して実行します タスクはエスカレートされた 優先度を保持します 優先度のエスカレーションを 元に戻すことはできません 迅速なサービスでVIP客に満足してもらい 良いレビューを得れたため キッチンの人気が上昇し始めました リソースを効果的に使用しているか 確認すると 材料を切る作業を多数 行ってることに気付きました タスクグループの並行処理を管理するため 役立つパターンを調べてみましょう まな板の数には 限りがありますよね 同時に多くの材料を切ると 他タスクのためのスペースがなくなるため 同時に切る材料数を制限します コードに戻って 切るタスクを作成する ループを調べます 各材料ごとの元のループを 最大数の切るタスクを 開始するループに置き換えます 次に 前のタスクが終了するたびに 新しいタスクを開始するための 結果を収集するループが必要です 新しいループは いずれかのタスクかが 終了するのを待ち まだ切る必要のある材料がある場合に 次の材料を切る 新しいタスクを追加します これを明確にするために パターンを整理しましょう 最初のループでは 最大数の並行タスクを作成し 過剰に 作成しないようにします 最大数のタスクが実行すると 1つのタスク終了を待ちます 終了後 停止条件に 達していない場合は 新しいタスクを作成して 進行を続けます 前のタスクが終了するまで 新しい作業を開始しないため これにより並行タスクの 数が制限されます 先程調理スタッフがシフトで働き キャンセルを使用して シフト終了を示すことに ついて話します これはシフト処理コード Kitchen Service です 各調理スタッフは個別のタスクで シフトを開始します 調理スタッフが働き始めたら タイマーを開始します 終了すると進行中のシフトを キャンセルします どのタスクも値を返しません withDiscardingTaskGroup APIが新しくあります タスクグループを破棄すると 完了した子タスクの結果を保持されません タスクで使ったリソースは すぐに解放されます ランメソッド変更すれば 破棄するタスクグループを利用できます タスクグループの破棄は自動的に その子タスクも消してくれるので 明示的にグループをキャンセルして クリーンアップする必要はありません タスクグループの破棄は 兄弟タスクも自動的にキャンセルします 子タスクのいずれかが エラーをスローすると 残りのタスクが自動的にキャンセルされます これは私たちのユースケースに理想的です シフト終了時に TimeToCloseErrorをスローでき すべての調理スタッフの シフトが自動的に終了します 新しいタスクグループの破棄は 普通のタスクグループと異なり タスクの結果を 収集する必要がないため タスク終了するとリソースが 自動的に解放されます これはリクエストの ストリームを処理するような 値を返す必要ない多くのタスクがある場合 メモリの消費を減らすのに役立ちます 特定の状況ではタスクグループから 値を返したいけれど 同時に並行実行されるタスクの数を 制限したいことがあります 1つのタスク完了を持って別のタスクを 開始することで タスクの過多を回避できます 今まで以上に効率的に スープを作れるようになりましたが さらに規模を拡大する必要があります 製造をサーバに移す時がきました それには処理される注文を追跡するという 課題が伴います これにはタスクローカル値が役立ちます タスクローカル値は特定タスク あるいは より正確にはタスクの階層と 関連付けられたデータの一部です タスクローカル値にバインドされた値は 現在のタスク階層からのみ 利用可能です タスクローカル値は TaskLocal プロパティラッパーを使用して 静的プロパティとして宣言されます タスクローカル値はオプショナルに するのが良いプラクティスです 値のセットがないタスクは デフォルト値を返す必要があります これは nil オプショナルで 簡単に表現できます アンバウンドタスクローカルは デフォルト値を含みます オプショナルな String が あるので nil であり 現在のタスクに関連付けた 調理スタッフはいません タスクローカル値は 明示的に割り当てることはできない一方 特定のスコープにバインドする 必要があります バインディングはスコープの期間中続き スコープの終わりで元の値に戻ります タスクツリーに戻ると 各タスクにタスク ローカル値に関連付けられた場所があります makeSoupの前に 「cook」タスクローカル変数に 「Sakura」という名前をバインドします makeSoupだけが バウンドされた値を保存します 子タスクはタスクローカルストレージに 値を一切保存しません タスクローカル変数にバインドされた 値を見つけるには その値を持つタスクを見つけるまで 親を再帰的にたどる必要があります バインドされた値のタスクを見つけた時 タスクローカルは値を受入れます ルートに到達した場合 つまりそれ以上の親タスクがない場合 タスクローカルはバインドせず オリジナルのデフォルト値が取得されます Swiftランタイムはこれらのクエリを 高速に実行すべく 最適化されています ツリーをたどるのではなく 探しているキーを持つタスクへの 直接的な参照ができます タスクツリーの 再帰的な性質は以前の値を 失うことなく値を シャドウするのに最適です スープ作りの現ステップを 辿りたいとしましょう makeSoupで step変数を soup にバインドし chopIngredients で chopに再バインドできます chopIngredients で バインドされた値は chopIngredientsから値を返すまで 以前の値をシャドウし 元の値を引き継ぎます ビデオ編集の魔法により 私達は要求に応えるために サービスをクラウドに 移動しました 私たちのスープ作りの機能は変わらずに ただサーバ上に移動しただけです 注文の進行状況をモニタリングし 予定通り期限内に完了しているか 予期しない障害を監視するために 注文をモニタリングする必要があります サーバ環境では多くの リクエストが並行処理されるため 特定の注文を追跡するための 情報を含める必要があります 手動でのログ記録は冗長で エラー等が発生しやすいです おっと 誤ってすべての 注文を記録してしまいました タスクローカル値を使ってログ作業の 信頼性を高める方法を見てみましょう Appleデバイスでは引き続きOSLog APIを 直接使う必要がありますが アプリの一部がクラウドに移行する上で 他の解決策が必要になります SwiftLog は 様々なバックエンド実装を持つ ログ記録のAPIパッケージで サーバに変更を加えることなく 必要に応じて ログバックエンドを導入できます MetadataProvider は SwiftLog 1.5の新しいAPIです メタデータプロバイダを実装すると ログに関するロジックの抽象化が容易になり 関連する値について一貫した情報を 出力できるようになります メタデータプロバイダは 辞書のような構造を利用して ログされる名前を値にマッピングします orderIDタスクローカル変数を自動的にログし それが定義されているかを確認し 定義されていれば辞書に追加します 複数のライブラリが独自の メタデータプロバイダを定義する場合 ライブラリ固有の情報を探し 複数のメタデータプロバイダを 単一オブジェクトに結合する multiplex関数を定義します メタデータプロバイダを使用して 該当プロバイダのログシステムを初期化し ログの記録を開始します メタデータプロバイダで 指定した情報は 自動的にログに含まれるため ログメッセージに含める必要はありません ログにはオーダー0が キッチンに入る様子や 調理スタッフがそのオーダーを 受ける箇所が表示されます メタデータプロバイダの値は ログに明確にリスト化されるため スープ作りの過程で注文を追跡するのが 容易になります タスクローカル値はタスクの階層に情報を 関連付けることを可能にします デタッチされたタスクを除いた全タスクは 現タスクのタスクローカル値を継承します この値は特定のタスクツリーの スコープにバインドされ タスク階層を通じて 追加のコンテキスト情報をプロパゲートする 低レベルなビルディングブロックを提供します 次にタスク階層と そのツールを使用して 並行分散システムトレースと プロファイリングを行います Appleプラットフォームで 並行処理を行う場合 Instruments が役立ちます Swift Concurrency Instruments は 構造化タスク間の関係性への インサイトを提供します 詳細はこちらをご覧下さい "Visualize and optimize Swift concurrency" "Analyze HTTP Traffic in instruments" で確認できる通り HTTPトラフィックInstruments も導入されました HTTPトラフィックアナライザは ローカルで発生するトレースのみを表示します プロファイルでサーバからの レスポンスを待つ間は グレーのボックスが表示されるので サーバのパフォーマンスを改善するためには さらなる情報が必要です そこで Swift Distributed Tracing パッケージを導入します タスクツリーは 単一のタスク階層で 子タスクを管理するのに便利です 分散トレーシングを活用することで 複数のシステム上で タスクツリーの利点を生かし パフォーマンス特性やタスクの関係性を 把握することができます Swift Distributed Tracing パッケージは OpenTelemetry プロトコルをサポートし ZipkinやJaegerなど既存のトレーシング ソリューションとの互換性があります Swift Distributed Tracingは Xcode Instrumentsの「応答待ち」の 不透明な部分を埋め合わせ サーバで何が起こっているかについての 詳細情報の提供を目指しています フォーカスすべき部分を特定できるよう サーバコードに計測機能が必要です 分散トレーシングは ローカルの トレーシングプロセスとは少し異なります 関数ごとにトレーシングを取得する代わりに withSpan APIを利用し スパンで コードをインストルメント化します スパンはトレーシングシステムで 報告されるコードの特定の領域に 名前を付けることができます スパンは関数全体を カバーする必要はありません スパンは特定関数に関する 詳しいインサイトを提供できます withSpan は追加の トレースIDやその他のメタデータで タスクに注釈を加え複数のタスクツリーを 単一のトレースに統合します トレースシステムには タスクの階層に関する インサイトやタスクの実行時の パフォーマンス特性などの 情報が十分に備わっています スパン名はトレースのUIに表示されます スパン名は短く説明的で 簡潔である必要があります スパン名に注文IDを含める必要はなく スパン属性を使用して 追加のメタデータを添付できます ここでは関数名を #functionディレクティブで 自動的にスパン名に設定し スパン属性を使って現注文のID を スパン情報に添付し トレーサーに報告するようにしました トレースシステムは通常スパンを検査する際に 属性を表示します スパンにはたいてい HTTPステータスコード リクエストと応答のサイズ 開始/終了時間などのメタデータが含まれ 情報の追跡が容易になります 独自の属性を定義することもできます 詳しいスパンの活用例は swift-distributed-tracing-extras リポジトリをご参照下さい タスクが失敗しエラーをスローすると その情報はスパンと トレースシステムに表示されます スパンはタイミング情報と タスクツリー内の関係を含み タイミング競合によるエラーや それが他のタスクに どう影響を与えるかを 特定するのに役立つ方法です トレースシステムや タスクツリーの再構築方法 スパンへの属性の追加を 説明しましたが まだ分散システムへの 組み込みは行っていません トレースシステムでは これ以上の作業は必要ありません 材料を切るサービスを キッチンサービスから分離しても 同じコードを使用する場合 トレースシステムは 自動的にトレースを収集し 分散システム内の異なるマシン間で 関連付けます トレースビューではスパンが 別のマシン上で 実行されていることが 示されますが 内容は同じです HTTPクライアントやサーバ RPCシステムを含む システム全般でトレースを利用すると パワフルな分散トレーシングが 可能になります Swift Distributed Tracingは タスクツリーと タスクローカル値を使用し 信頼性の高い クロスノードトレースを実現するための すべての情報を自動プロパゲートします 構造化タスクは 並行システムの潜在力を解放し 自動キャンセルや 優先度情報の自動プロパゲート 複雑な分散ワークロードの トレースを容易にする ツールを提供します これらは Swiftの 構造化並行処理の 性質によって可能になっています このセッションで構造化 並行処理について関心を高め 非構造化された代替手段を使用する前に 構造化されたタスクを 活用していただければ幸いです ご視聴ありがとうございました 構造化並行処理を使って 役立つパターンを ぜひ生み出してください うーん美味しそうなスープ! ♪音楽♪
-
-
2:27 - Unstructured concurrency
func makeSoup(order: Order) async throws -> Soup { let boilingPot = Task { try await stove.boilBroth() } let choppedIngredients = Task { try await chopIngredients(order.ingredients) } let meat = Task { await marinate(meat: .chicken) } let soup = await Soup(meat: meat.value, ingredients: choppedIngredients.value) return await stove.cook(pot: boilingPot.value, soup: soup, duration: .minutes(10)) }
-
2:42 - Structured concurrency
func makeSoup(order: Order) async throws -> Soup { async let pot = stove.boilBroth() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
3:00 - Structured concurrency
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if choppedIngredient != nil { choppedIngredients.append(choppedIngredient!) } } return choppedIngredients } }
-
4:32 - Task cancellation
func makeSoup(order: Order) async throws -> Soup { async let pot = stove.boilBroth() guard !Task.isCancelled else { throw SoupCancellationError() } async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
4:58 - Task cancellation
func chopIngredients(_ ingredients: [any Ingredient]) async throws -> [any ChoppedIngredient] { return try await withThrowingTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in try Task.checkCancellation() // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for try await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
5:47 - Cancellation and async sequences
actor Cook { func handleShift<Orders>(orders: Orders) async throws where Orders: AsyncSequence, Orders.Element == Order { for try await order in orders { let soup = try await makeSoup(order) // ... } } }
-
6:41 - Cancellation and async sequences
public func next() async -> Order? { return await withTaskCancellationHandler { let result = await kitchen.generateOrder() guard state.isRunning else { return nil } return result } onCancel: { state.cancel() } }
-
7:40 - AsyncSequence state machine
private final class OrderState: Sendable { let protectedIsRunning = ManagedAtomic<Bool>(true) var isRunning: Bool { get { protectedIsRunning.load(ordering: .acquiring) } set { protectedIsRunning.store(newValue, ordering: .relaxed) } } func cancel() { isRunning = false } }
-
10:55 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients for ingredient in ingredients { group.addTask { await chop(ingredient) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:01 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients let maxChopTasks = min(3, ingredients.count) for ingredientIndex in 0..<maxChopTasks { group.addTask { await chop(ingredients[ingredientIndex]) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] for await choppedIngredient in group { if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:17 - Limiting concurrency with TaskGroups
func chopIngredients(_ ingredients: [any Ingredient]) async -> [any ChoppedIngredient] { return await withTaskGroup(of: (ChoppedIngredient?).self, returning: [any ChoppedIngredient].self) { group in // Concurrently chop ingredients let maxChopTasks = min(3, ingredients.count) for ingredientIndex in 0..<maxChopTasks { group.addTask { await chop(ingredients[ingredientIndex]) } } // Collect chopped vegetables var choppedIngredients: [any ChoppedIngredient] = [] var nextIngredientIndex = maxChopTasks for await choppedIngredient in group { if nextIngredientIndex < ingredients.count { group.addTask { await chop(ingredients[nextIngredientIndex]) } nextIngredientIndex += 1 } if let choppedIngredient { choppedIngredients.append(choppedIngredient) } } return choppedIngredients } }
-
11:26 - Limiting concurrency with TaskGroups
withTaskGroup(of: Something.self) { group in for _ in 0..<maxConcurrentTasks { group.addTask { } } while let <partial result> = await group.next() { if !shouldStop { group.addTask { } } } }
-
11:56 - Kitchen Service
func run() async throws { try await withThrowingTaskGroup(of: Void.self) { group in for cook in staff.keys { group.addTask { try await cook.handleShift() } } group.addTask { // keep the restaurant going until closing time try await Task.sleep(for: shiftDuration) } try await group.next() // cancel all ongoing shifts group.cancelAll() } }
-
12:41 - Introducing DiscardingTaskGroups
func run() async throws { try await withThrowingDiscardingTaskGroup { group in for cook in staff.keys { group.addTask { try await cook.handleShift() } } group.addTask { // keep the restaurant going until closing time try await Task.sleep(for: shiftDuration) throw TimeToCloseError() } } }
-
14:10 - TaskLocal values
actor Kitchen { @TaskLocal static var orderID: Int? @TaskLocal static var cook: String? func logStatus() { print("Current cook: \(Kitchen.cook ?? "none")") } } let kitchen = Kitchen() await kitchen.logStatus() await Kitchen.$cook.withValue("Sakura") { await kitchen.logStatus() } await kitchen.logStatus()
-
16:17 - Logging
func makeSoup(order: Order) async throws -> Soup { log.debug("Preparing dinner", [ "cook": "\(self.name)", "order-id": "\(order.id)", "vegetable": "\(vegetable)", ]) // ... } func chopVegetables(order: Order) async throws -> [Vegetable] { log.debug("Chopping ingredients", [ "cook": "\(self.name)", "order-id": "\(order.id)", "vegetable": "\(vegetable)", ]) async let choppedCarrot = try chop(.carrot) async let choppedPotato = try chop(.potato) return try await [choppedCarrot, choppedPotato] } func chop(_ vegetable: Vegetable, order: Order) async throws -> Vegetable { log.debug("Chopping vegetable", [ "cook": "\(self.name)", "order-id": "\(order)", "vegetable": "\(vegetable)", ]) // ... }
-
17:33 - MetadataProvider in action
let orderMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let orderID = Kitchen.orderID { metadata["orderID"] = "\(orderID)" } return metadata }
-
17:50 - MetadataProvider in action
let orderMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let orderID = Kitchen.orderID { metadata["orderID"] = "\(orderID)" } return metadata } let chefMetadataProvider = Logger.MetadataProvider { var metadata: Logger.Metadata = [:] if let chef = Kitchen.chef { metadata["chef"] = "\(chef)" } return metadata } let metadataProvider = Logger.MetadataProvider.multiplex([orderMetadataProvider, chefMetadataProvider]) LoggingSystem.bootstrap(StreamLogHandler.standardOutput, metadataProvider: metadataProvider) let logger = Logger(label: "KitchenService")
-
18:13 - Logging with metadata providers
func makeSoup(order: Order) async throws -> Soup { logger.info("Preparing soup order") async let pot = stove.boilBroth() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) }
-
20:30 - Profile server-side execution
func makeSoup(order: Order) async throws -> Soup { try await withSpan("makeSoup(\(order.id)") { span in async let pot = stove.boilWater() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) } }
-
21:36 - Profiling server-side execution
func makeSoup(order: Order) async throws -> Soup { try await withSpan(#function) { span in span.attributes["kitchen.order.id"] = order.id async let pot = stove.boilWater() async let choppedIngredients = chopIngredients(order.ingredients) async let meat = marinate(meat: .chicken) let soup = try await Soup(meat: meat, ingredients: choppedIngredients) return try await stove.cook(pot: pot, soup: soup, duration: .minutes(10)) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。