ロバメモ - 素人のUnity覚書と奮闘記

素人のUnity覚書と奮闘記

デリゲート,コールバック,ラムダ式,非同期とやらを調べてみた⑤非同期

前記事: デリゲート,コールバック,ラムダ式,非同期とやらを調べてみた④ラムダ式 - ロバメモ - 素人のUnity覚書と奮闘記

非同期とは

この記事読めば大体わかる
tech-lab.sios.jp
このブログは自分のアウトプットの場でもあるので、自分なりに整理してみる。

非同期という言葉があるからには、同期という言葉もある。
処理を待って次の処理を行うことを同期というらしい。つまり、流れに沿って一つずつこなしていくタイプのプログラムになる。 これに対して、バックグラウンドで実行されるプログラムを非同期という。複数のプログラムを同時に動かすことができる。マルチスレッドとかマルチタスクといった言葉を聞いたことがあるならそれがこれに当たる。

同期の処理の場合:
  処理A → 処理B → 処理C → 処理D
非同期の場合:
  処理A → コールバック
  処理B → 処理Aの完了を待って(同期を取って) → 処理C
  処理D

こんな感じで非同期だと同時に複数のことができる。

Taskを使って非同期処理を作る

Threadを使う方法とTaskを使う方法があるらしいが、Threadのほうは古いやり方らしいので、Taskのみをメモすることにした。

書式

Task.Run メソッド (System.Threading.Tasks) | Microsoft Docs
公式見ると、引数の種類が色々あるみたいだけど、端的に書いてみる。

//定義  varでも可能
Task<戻り値の型> タスクの名前;

//インスタンス生成&実行
タスクの名前 = Task.Run(デリゲート);

//一行で書くなら
Task<戻り値の型> タスクの名前 = Task.Run(デリゲート);

※デリゲートの部分は、大抵がラムダ式を使って書かれてる。

戻り値がない場合

3秒後にCallbackとコンソールに表示されるコード。

//デリゲートが参照するメソッド
void Callback()
{
    Thread.Sleep(3000);  //3秒待機
    Debug.Log("Callback");
}

void Start()
{
    //非同期処理を実行
    Task task = Task.Run(() => Callback());
}

戻り値がある場合

3秒後にコンソールにTrueと表示されるコード。

//デリゲートが参照するメソッド
bool Callback()
{
    Thread.Sleep(3000);  //3秒待機
    return true;
}

void Start()
{
    //非同期処理を実行
    Task<bool> task = Task.Run(() => Callback());
    Debug.Log(task.Result);
}

task.Resultと書くだけで、処理を待って戻り値を返してくれる。

Thread.Sleep(3000);について

3秒間待機するコード。例では擬似的に思い処理を表現するために使用されているけど、Threadを使っているので古い記述になるみたい。今の所エラーはでてないけどね。
じゃぁ、タスクを使った書き方は?

//Threadの場合
bool Callback()
{
    Thread.Sleep(3000);  //3秒待機
    Debug.Log("Callback");
    return true;
}

//Taskの場合
async Task<bool> Callback()
{
    await Task.Delay(3000);  //3秒待機
    Debug.Log("Callback");
    return true;
}

いや、違い過ぎるやん!困惑w

async / await

読み方と意味

asyncは”エイ シンク”と読むそうです。syncは英語で”同調”という意味の不可算名詞だそうです。それにaつけてるんですね。a coffee(一杯のコーヒー)的な感じだろうか?いや、”ア”と呼ばずに”エイ”と呼ぶので、aは不定冠詞じゃないかもしれない。
コンピューター用語としては、syncは”同期する”という意味だそうです。
awaitは、"アウェイト"と読み、英語で”〜を待つ”という他動詞だそうです。これはそのままの意味っぽいね。

使い方

async Task<戻り値の型> メソッド名Async()
{
    await 完了を待ちたい他の非同期処理
    //なんらかの処理
    return 戻り値;
}

async および await を使用したタスク非同期プログラミング (TAP) モデル (C#) | Microsoft Docs
このC#公式ドキュメントからの抜粋

次の特徴は、前の例を非同期のメソッドにするための概略です。
・メソッド シグネチャは async 修飾子を含みます。
・非同期メソッドの名前は、慣例により「Async」というサフィックスで終わります。
・戻り値の型は次のいずれかになります:
  ・メソッドが、オペランドに TResult 型を持つステートメントを戻す場合、Task
  ・メソッドがステートメントを戻さない、またはオペランドを持たないステートメントを戻す場合、Task。
  ・非同期のイベント ハンドラーを作成する場合、void。
  ・GetAwaiter メソッドがあるその他の任意の型 (C# 7.0 以降)。
  詳細については、「戻り値の型およびパラメーター」セクションを参照してください。
・メソッドには、通常は 1 つ以上の await 式があり、待機中の非同期操作が完了するまでメソッドを続行できないポイントをマークします。
 この間メソッドは中断し、メソッドの呼び出し元にコントロールを戻します。

見慣れないカタカナ多くて読みづらいわ!と思いつつ、我慢して読むべし。
要するに・・・

  • メソッドにasyncをつけたら”これは非同期メソッドですよ”って意味になるよ。
  • メソッド名の最後に"Async"ってつけてね。なんでって?慣例なんだよ。お約束なの。
  • 戻り値はTask<戻り値の型>になるよ。
  • 戻り値がない場合は、Taskって書いてね。
  • メソッドが非同期のイベントハンドラーの場合はvoid使っていいよ。
  • 完了を待ちたい他の非同期メソッドがGetAwaiterとやらを持ってるならその型を戻り値とするよ。(たぶん)
  • メソッドに最低一つはawaitを書いてね。
  • await書いたら非同期処理が完了するまで、このメソッドは中断するよ。その間はこのメソッドの呼び出し元にコントロールを返すから、中断中に何かしたい場合は呼び出し元のほうに書いてね。(たぶん)

ってな感じですかね〜?ちょっとあやふやな部分あるけど。苦笑。
asyncにしたらawait書かないといけないってことから、非同期メソッドを作る目的が他の非同期処理の完了待ちすることにあるってのがわかる。
非同期処理Aの完了を待つための非同期メソッドBって感じかな。

結局のところコルーチンでよくない?

じゃぁ、実際のところ、どういう場面で使うのかなって考えた場合、例えばシーンやらアセットバンドルやらを読み込む時とかかなぁと思ったわけですよ。
で、Unityで用意されているシーンを非同期で読み込むメソッドSceneManager.LoadSceneAsync()のドキュメントを覗いてみる。
SceneManagement.SceneManager-LoadSceneAsync - Unity スクリプトリファレンス
名前にAsync入ってますね〜!ん?ということは、このメソッドの処理のどこかでawaitが使われているということなのね。
え〜っと、完了を知るにはどうすればいいの?
公式ドキュメント見ると、戻り値はUnityEngine.AsyncOparationになってるんで、そのドキュメントをみて見ると・・・
UnityEngine.AsyncOperation - Unity スクリプトリファレンス

説明のところに”非同期動作のコルーチンで使用します”って書いてる。ってことはこれらの処理を待ちたいときはコルーチンで処理しろってことのようです。
LoasSceneAsync()のドキュメントに戻ると、コルーチンを使って完了を待つコードが例として書かれてる。
await使わんのか〜い!
でね、await使えないのかと四苦八苦してみたんだけど、無理だった。笑
調べてみるとUniTaskとやらを使うとコルーチンじゃなくてawaitで待てるっぽい。
UniTaskって、また新しい用語でてきたし。困惑w
そういうパッケージがあるそうで、インストールして使うんだそうです。もう心折れちゃうよ。

関連記事

Delegate
ActionとFunc
コールバック
ラムダ式
非同期