📌

Verse言語の設計思想を読み解きたい(11)非同期処理③ block式/sync式

2023/04/13に公開

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

今回はblock式とsync式について

block式(イミディエイト処理式)

block式(block expression)」は、コード中に「コードブロック(code block)[1]」を作れるイミディエイト処理式[2]です。

block:
    funcA()
    funcB()
    funcC()

block演算子の後にある":"から下の、半角空白4個でインデントされた行の範囲がコードブロックになります。

block式のコードブロックに記述された式は、通常の式と同じく頭から順番に実行されます。コードブロック内で定義された定数/変数のスコープは、そのコードブロック内に限定されます。

block式は値を返します。返し値は、コードブロックで最後に評価された式の値となります。

以下のサンプルコードでは、block式内で複数個の非同期式を実行しています。これは、次に説明するsync式との比較の為であり、このblock式自体には意味がありません。

# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
    func1 := block:                 #<--block expression
        AsyncFunction("A", 5)
        Print("B")
        AsyncFunction("C", 2)
        Print("D")
    Print("end")
    Sleep(10.0)

AsyncFunction(name : string, count : int)<suspends>: void =
    for(x:=1..count):
        Print("async function:{name} {x}")
        Sleep(0.0)

OnBegin()関数はVerseのエントリポイントだと考えてください。OnBegin()関数はsuspendsエフェクト指定子が付与されているので非同期関数になります。

block式のコードブロックには、AsyncFunction()非同期関数とPrint()関数呼び出しが交互に記述されています。

AsyncFunction()関数(こちらも非同期関数です)は、「文字列を出力してからサスペンドする」を指定回数繰り返します。

Sleep()関数は指定した秒数だけ処理を中断するサスペンド式です。ここでは0.0秒を指定して、中断の後、直後のシミュレーションアップデートから再開するようにしています。

block式が終了すると値(Print("D")の戻り値)がfunc1に返ります。ただし、Print()関数はvoidなので意味はありません(他の並行処理式においての確認の為にこうしてあります)。

Print("end")は、いつ処理が次に進んだのかを確認する為に書いています。

最後に10秒間スリープするのは、コードブロック内の非同期処理が終了する前にOnBegin()関数自体が終了しないようにするためです(これもこの後の確認の為で、block式には関係ありません)。

このコードの実行結果は以下になります。

LogVerse: : async function:A 1 #<--- 1st update
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
LogVerse: : end                #<--- 8th update

コメントで、シミュレーションアップデートの区切りを追加しています[3]

block式内の処理が順番に実行され、全ての処理が終わった後に"end"が出力されます。Sleep()関数による中断は行われますが、コードブロック内の処理が同時に実行される訳では無い事がわかります。

以下、並行処理式の場合にこの挙動がどう変化するのかを見ていきます。

sync式(構造化並行処理式)

https://dev.epicgames.com/documentation/ja-jp/uefn/sync-in-verse
sync式(sync expression)」は、コードブロック内の非同期式を同時に実行できる(≒並行処理できる)並行処理式です。

sync式を記述するにはコードブロック内に2個以上の非同期式が必要です。0~1個の場合はコンパイルエラーになります。イミディエイト式を複数記述する事も可能です[4]

sync式が評価されると、コードブロックの各式が並行処理されます。全ての式が完了したら、sync式自身も終了し、次の行に処理が進みます。

sync式は値を返します。返し値は、コードブロックのそれぞれの式の返し値からなるタプル型です。

先程のサンプルコードをsync式に置き換えて実行してみます(変更箇所のみ抜粋します)。

    func1 := sync:                 #<--sync expression
        AsyncFunction("A", 5)
        Print("B")
        AsyncFunction("C", 2)
        Print("D")

"block"を"sync"に置き換えただけです。結果は以下になります。

LogVerse: : async function:A 1
LogVerse: : B
LogVerse: : async function:C 1
LogVerse: : D                  #<--- 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 
LogVerse: : end                #<--- 5th update

最初のアップデートでsync式内の4個の式が全て評価され、以後順に実行されているのが分かります。block式の出力結果と比較して、並行処理式の挙動を確認してみてください。

1st updateと2nd upadateについて詳しく見てみると、実際の処理順は以下の様になっています。

  • 1st update
    • AsyncFunction("A", 5)を実行→中断
    • Print("B")を実行
    • AsyncFunction("C", 2)を実行→中断
    • Print("D")を実行
  • 2nd update
    • AsyncFunction("A", 5)を再開→中断
    • AsyncFunction("C", 2)を再開→中断

"end"が一番最後に出力されている事から、sync式が終わるまで処理が先に進んでいない事が分かります。

次回はrace式/rush式の予定です。

続き

https://zenn.dev/t_tutiya/articles/a1683016d42d28


お知らせ

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

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

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

宣伝

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

脚注
  1. C#で言うスコープに相当 ↩︎

  2. このブログの造語です。前回記事参照。 ↩︎

  3. 正確ではないかもしれません ↩︎

  4. ただし、非同期式が2個以上ある事が前提 ↩︎

Discussion