🎃

Verse言語の設計思想を読み解きたい(9)非同期処理① 並行処理と並列処理

2023/04/11に公開

前回はこちら
https://zenn.dev/t_tutiya/articles/488e4a0d7e119d

前回まででVerse言語の最大の特徴と言える「失敗コンテキスト」について一通り説明しました。今回からはVerse言語のもう一つの大きな特徴である「非同期処理」を見ていきます。
https://dev.epicgames.com/documentation/ja-jp/uefn/concurrency-overview-in-verse
今回は本編に入る事前準備として、用語の整理と、非同期処理を扱う時に気を付けておきたい「並行処理」と「並列処理」の違いについて、そして何故非同期処理が重要なのかについて見ていきます(結果、Verse言語についてはほとんど触れていません)。

注意:非同期処理関連の訳語について(★このブログだけの話★)

日本語版公式ドキュメントにおいて、非同期処理周りの用語の訳が揺れていたり、一般的なプログラミング用語の訳語と異なっている場合がありました。

このブログでは以下のよう独自の訳を使用します。特に、非同期処理では「並行(concurrent)」と「並列(parallel)」は異なる概念なので、可能な限り明確に訳し分けることにします。並行と並列の違いについては次項で説明します。

原文 公式訳 このブログ
Parallel 同時 並列
Parallelly (※使用例無し) 並列に
Parallelism (※使用例無し) 並列処理
Concurrent 並列 並行
Concurrently 同時に 並行に
Concurrency 並列処理 並行処理
Concurrency Expression [1] 並列処理式 並行処理式
Structured Concurrency Expression 構造化並列処理式 構造化並行処理式
Unstructured Concurrency Expression 非構造化並列処理式 非構造化並行処理式

対応が分かりやすくなるように、公式ドキュメントで使用例が無い用語についても載せています。公式訳と異なる箇所は適宜読み替えてください。

また、以下の用語についてもこのブログ独自の訳を使います。

原文 公式訳 このブログ
Async Expression async式 非同期式
Async Function async関数 非同期関数
Async Context asyncコンテキスト 非同期コンテキスト
mutual exclusion 相互排除 排他制御
Time Flow タイムフロー 時間フロー[2]
Simulation Update シミュレーションの更新 シミュレーションアップデート

「失敗許容」と同じく、やってみて上手くいかなかったりしっくりこなかったら適宜更新していきます。

「並行処理」と「並列処理」

複数の処理が同時に実行される事をコンピュータープログラミングでは一般に「非同期処理(Asynchronous processing)」と言います。非同期処理は実行形式によって「並行処理(concurrency)」と「並列処理(parallelism)」とに分類されます。

Verseがユーザーに提供する非同期処理は並行処理(concurrency)で動作しています。両者の違いを見てみましょう。

並行処理(concurrency)

並行処理は、複数の処理をそれぞれ細かく分割し、細切れになった処理を切り替えながら実行することで、複数の処理を並行で進行させます。逆に言うと、分割された個々の処理は、あくまで短い時間の間に順繰りに実行されているのであり、本当に同時に実行されているわけではありません。

並列処理(parallelism)

並列処理は、マルチコアCPUにおいて、各コアごとに異なる処理を割り当てることで、複数の処理を同時に実行します。並行処理と異なり、個々の処理は文字通り同時に実行されます。

正確な表現ではありませんが、並行処理はソフトウェア的処理、並列処理はハードウェア的処理と考える事もできます。

「プリエンプティブ・マルチタスク」と「ノンプリエンティブ・マルチタスク」

並行処理は、複数の処理がどのように切り替わるかによって「プリエンプティブ・マルチタスク(preemptive multitasking)」と「ノンプリエンティブ・マルチタスク(non-preemptive multitasking)」に分類されます。

実行中の処理を、処理の外側(OSなど)が強制的に一時中断して別の処理に切り替える事を「プリエンプション(preemption[3]」と言います。プリエンプションによって複数処理の同時実行(=マルチタスク処理)が行われる形式を「プリエンティブ・マルチタスク」と呼びます。逆に、処理側が明示的に切り替える形式を「ノンプリエンティブ・マルチタスク[4]」と言います。

プリエンティブ・マルチタスクはハードウェアやOSの機能で実現される事が一般的です。
プリエンティブの場合、処理の切り替えは強制的に行われるため、プログラム側で対応する必要がありません。逆に、途中で切り替わったら困る一連の処理(「アトミック(atomic)な処理」と言います)がある場合、セマフォやミューテックスと呼ばれる手法を用いて、一時的に処理の切り替えを禁止する必要があります。
通常、マルチスレッドと呼ばれる機能がプリエンティブ・マルチタスクに該当します。

ノンプリエンティブ・マルチタスクはソフトウェアのみで実現できます。
ノンプリエンティブの場合、自身が明示しない限り処理の切り替えが発生しないので、アトミックな処理の管理が容易です。ただし、処理の1つがバグで無限ループに陥った場合に、他の処理に切り替わらないためにプログラム全体がフリーズしてしまうなどの危険性があります。
通常、コルーチンと呼ばれる機能がノンプリエンティブ・マルチタスクに該当します。

Verseがユーザーに提供している並行処理はノンプリエンティブ・マルチタスクで動作しています。

ゲームプログラミングにおいて非同期処理が重要である理由

ゲームプログラミング(あるいはメタバースプログラミング)では複数のオブジェクトが同時に様々な挙動を取るので、非同期処理対応は必須と言えます。

また、ゲームプログラミングでは、一つのオブジェクトが複数の処理を同時に行う事が頻繁にあります。
例えばアニメーションするボタンがある場合、「ボタンをループでアニメーションさせる」「ユーザーがボタンを押すのを待つ」という2つの処理が同時に行われます。ボタンが押されたらアニメーションを停止する必要があり、2つ処理は連携して動作する事になります。

更に言えば、ゲームプログラミングでは、1フレーム分の描画を単位[5]として、繰り返し処理が行われます。
全てのオブジェクトは1フレーム中に呼び出し元に処理を返す必要があり、次のフレームで再度呼びだされます。当然その際、オブジェクトは前フレームからパラメータを引き継いでいなければなりません(オブジェクトが状態を保持し続ける事を「ステートフル」と言います)。

このようにゲームプログラミングは、非同期かつステートフルに実行されるオブジェクトが大量に存在し、かつ、個々のオブジェクトも内部で複数の処理が非同期に実行されるという複雑な構造を持つ傾向にあります。

このような処理を、シンプルな(言い換えれば、メンテナンス性の高い)コードで実装するのは中々に大変です。この事はゲームプログラミングに限らず、現代的なGUIプログラミングにおいても共通の課題と言えると思います。

近代的なプログラミング言語では、この課題に対処するために、言語仕様に非同期処理を組み込んでいる物が多くあります(C#で言えばasync/awaitやreturn yieldなど)。
Verseはゲームプログラミング(あるいはメタバースプログラミング)を前提としている事もあり、非同期処理への対応に特に力を入れており、様々な仕様が採用されています。

次回から、Verseの非同期処理が持つ強力な能力を見ていきましょう。

続き

https://zenn.dev/t_tutiya/articles/3fd04df5af4160


お知らせ

verse言語とUEFNの記事を他にも書いているので御覧下さい。
https://zenn.dev/t_tutiya

最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

宣伝

「Unityシェーダープログラミングの教科書」シリーズ1~5をBOOTHで頒布中です。
https://s-games.booth.pm/

脚注
  1. concurrent expressionと記述されている場合も有ります ↩︎

  2. 処理フロー(Control Flow)の対応である事を明確にするため ↩︎

  3. ここでは「差し替える」の意味 ↩︎

  4. 協調的マルチタスク(Cooperative multitasking)」とも呼びます ↩︎

  5. Verseではこの1回分の描画処理のことを「シミュレーションアップデート」と呼びます ↩︎

Discussion