Verse言語の設計思想を読み解きたい(13)非同期処理⑤ branch式/spawn式/Taskオブジェクト

2023/04/15に公開

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

非同期処理シリーズ最終回です。

branch式(構造型並行処理式)

https://dev.epicgames.com/documentation/ja-jp/uefn/branch-in-verse
branch式(branch expression)」はコードブロックを取る並行処理式です。

branch式のコードブロックには少なくとも1個の非同期式を記述しなければいけません(0個だとコンパイルエラー)。イミディエイト式を記述することもできます。

branch式が評価されると、コードブロック内の処理と、branch式の次の処理が同時に(並行処理で)実行されます。コードブロックのコードは順番に実行されます[1]。block式の非同期版と考えると分かりやすいかもしれません。

branch式は結果を持たないため、結果の型は void になります(変数で受け取る場合はany型になります)。

サンプルコードです。式を"branch"にしている以外は前回と同じです。

func1 := branch:
    AsyncFunction("A", 5)
    Print("B")
    AsyncFunction("C", 2)
    Print("D")
Print("end")
Sleep(1.0)

実行結果は以下になります。

LogVerse: : async function:A 1 
LogVerse: : end                #<--- 1st update
#sleepが無い場合ここで終わり
LogVerse: : async function:A 2 #<--- 2nd update
LogVerse: : async function:A 3 #<--- 3rd update
LogVerse: : async function:A 4 #<--- 4th update
LogVerse: : async function:A 5 #<--- 5th update
LogVerse: : B
LogVerse: : async function:C 1 #<--- 6th update
LogVerse: : async function:C 2 #<--- 7th update
LogVerse: : D                  #<--- 8th update

branch式コードブロック内のAが実行された後に、branch式の次の処理であるPrint("end")が実行されています。その後、コードブロック内の処理が(並行処理ではなく)順番に実行されているのが分かります。

branch式の中身も並行処理させたい場合、以下の様に書く事もできます。

func1 := branch:
    sync:
        AsyncFunction("A", 5)
        Print("B")
        AsyncFunction("C", 2)
    Print("D")
Print("end")
Sleep(1.0)

この場合、結果は以下になります。

LogVerse: : async function:A 1
LogVerse: : B
LogVerse: : async function:C 1
LogVerse: : end                #<--- 1st update
LogVerse: : async function:A 2
LogVerse: : async function:C 2 #<--- 2nd update
LogVerse: : async function:A 3 #<--- 3rd update
LogVerse: : async function:A 4 #<--- 4th update
LogVerse: : async function:A 5 #<--- 5th update
LogVerse: : D                  #<--- 6th update

AとCが並行処理されているのがわかります。先程の結果と比較してみてください。

spawn式(非構造化並行処理式)

https://dev.epicgames.com/documentation/ja-jp/uefn/spawn-in-verse
spawn式(spawn expression)」はコードブロックに式を1つだけ取る並行処理式です。

spwan式のコードブロックに記述出来るのは1個の非同期式だけです(イミディエイト式を記述するとコンパイルエラー)。他の並行処理式と異なり、コードブロックで複数の式を受け取れません[2]。そのため「非構造化並行処理式(Unstructured Concurrency Expression)」と呼ばれます。

spawn式が評価されると、コードブロック内の非同期式と同時に(並行処理で)spawn式の次の処理が実行されます[3]。branch式のワンライナー版と考えるとわかりやすいかもしれません。

spawn式は他の並行処理式と異なり、非同期コンテキストでない場所(つまり、suspendsエフェクト指定子が付与されていない関数内)にも記述出来るという特徴があります。

spawn式は値を返します。返し値は、次項で説明するtask型になります。

サンプルコードです。spawn式では式を1つしか受けとれません。

func1 := spawn{AsyncFunction("A", 5)}

結果は以下になります。

LogVerse: : async function:A 1
LogVerse: : end
LogVerse: : async function:A 2
LogVerse: : async function:A 3
LogVerse: : async function:A 4
LogVerse: : async function:A 5

非同期で実行出来る式が1個のみである事を除けば、branch式と挙動が同じである事がわかります。

ちなみに、以下の様な記述はコンパイルエラーになります。

func1 := spawn:
    branch:
        AsyncFunction("A", 5)
        AsyncFunction("A", 5)

現時点ではspawn式内に複数の非同期処理が実行される記述は出来ません(将来的には対応予定のようです)。

なお、公式ドキュメントでは「可能な限りbranch式を使うべきである」とされています。

taskクラス

https://dev.epicgames.com/documentation/ja-jp/uefn/task-in-verse
タスク(Task)」は、並行処理されている最中の個々の非同期式の状態を示すオブジェクトです。一般的に、個々の処理自体を表す用語としても使用されます。

現verのVerseでは、taskオブジェクトを返す並行処理式はspawnだけです。
また、taskオブジェクトが公開している関数はAwait()のみで、その非同期式が完了するまで待機します。

サンプルコードです。spawn式の戻り値がtaskオブジェクトです。Print("end")の前にAwait関数呼び出しを挿入しています。

func1 := spawn:
    AsyncFunction("A", 5)
func1.Await()
Print("end")
Sleep(1.0)

結果は以下になります。

LogVerse: : async function:A 1
LogVerse: : async function:A 2
LogVerse: : async function:A 3
LogVerse: : async function:A 4
LogVerse: : async function:A 5
LogVerse: : end

Await()によって、Aが終わるまで待機し、その後で"end"が出力されているのが分かります。

続き

https://zenn.dev/t_tutiya/articles/0ed2f969a267ad


お知らせ

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

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

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

宣伝

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

脚注
  1. branchは「分岐」の意味 ↩︎

  2. 実際には、一行だけのコードブロックなら受け取れます ↩︎

  3. spawnは「発生」の意味 ↩︎

Discussion