最初に
どうも、ろっさむです。
引き続きUnity非同期を完全に理解するために今回はasync/await
について見ていこうと思います。
ちなみにこれが2018年最後の記事となります。お疲れ様でした。
Unity非同期完全に理解するための歩み:
- 【Part0】タスクとスレッドとプロセスの違いを知って非同期完全理解に近付く
- 【Part1】Unity非同期完全に理解するための第一歩~非同期処理とは何か~
- 【Part2】古来よりUnity非同期を実現していたコルーチンとは何者か?
- 【Part3】非同期理解のためにasync/awaitとTaskの基礎を学んだ話 ←今ココ!
async/await?
C#5.0以降から追加された機能です。Unity2018以降ではC#6.0が使用できるため、このasync/await
も使用することが可能です。
async/await
はTaskクラスと紐付いた言語構文の一つです。
多くの場面でシングルスレッド的な動作になり、lock不要となったりします。
await
とは「待つ」と言う意味で、await
をつけた処理は一旦別スレッドに制御を移した上で、処理完了後に続きの処理を再開します。「式」が書ける場所であればどこにでも書くことができます。C#6.0以降はcatch/finally
の中でもawait
演算子を使用することができます。
async
はawait
をメソッド内で使用する際に、メソッドにつける必要があるキーワードです(async:Asynchronous:非同期)。ただしメソッドにasync
をつけただけでは通常のメソッドと挙動は変わりません。実際に意味を持つのはawait
の方となります。
async/await
は**「処理を非同期的に行う仕組み」ではなく「非同期の処理を待つための仕組み」**となります。
構文としては以下のようになります。
async 戻り値の型 関数名( 引数 )
{
同期的な処理 または 非同期メソッドをawaitで待機するコード
}
asyncをつけたメソッドの戻り値は
①非同期の完了を待つ必要がない場合にvoid
②完了を待つ必要がある場合には非同期メソッドビルダー(Task、Task<T>、ValueTask<T>
など)を実装した型である必要がある(ValueTask<T>
はC#7.0から)
となります。ただvoidはメリットが薄いためそこまで使われていない印象です。
非同期メソッドビルダーの実装型
Taskクラスは便利な非同期メソッドが数多く用意されています。
Task
非同期メソッドの完了を待つ場合、返り値がない場合はTask
を用います。
async Task WaitMethod()
{
await Task.Delay(3000);
// returnステートメント不要
}
Task.Run()
引数として渡したデリゲートを別スレッドで実行するタスクを作成することができます。重たい処理をUIスレッドではなく別スレッドで行いたい場合に使用します。
Task.Run(デリゲート(ラムダ式など))
var x = 500;
// 重い処理なのでUIスレッドから逃す
await Task.Run(() =>
{
HeavyWork(x)
});
// ラムダ式を与えるコード
await Task.Run(() => {
Console.WriteLine("処理1");
Thread.Sleep(3000);
Console.WriteLine("処理2");
Thread.Sleep(3000);
});
Task.WhenAll()
全てのタスクが完了したら完了とするタスクを作成することができます。
var x = 500;
await Task.WhenAll(
HeavyWork1(x),
HeavyWork2(x),
HeavyWork3(x));
Task<T>
戻り値が欲しい場合はTask<T>
を使います。
private async Task<string> Hoge()
{
await Task.Delay( 3000 );
return "ほにゃー";
}
string result = await Hoge();
Console.WriteLine($"{result}");
ValueTask
C#7.0以降から使用できます。どんな場合に使用するかと言うと、まず以下のコードを確認してください。
async Task<string> Hoge(int num)
{
if(num == 100)
{
await Task.Delay(3000);
return "OK";
}
return "NG";
}
上記コードですと、num
の値が100の場合にのみ非同期処理が行われます。この場合に、ほんの一部分しか非同期処理が行われないのにTask(参照型)のインスタンスを生成するとコストが大きくなってしまいます。ここでValueTask構造体(値型)を使うことで非同期が必要な場合にのみ内部でTaskのインスタンスが作られるようになり、同期的処理がメインのメソッドのパフォーマンスが大幅に向上します。
なぜ、TaskとValueTaskで使い分けを行うかと言うと、WhenAll
がTaskしか受け取らないことが起因しています。Task.WhenAll
はneue.ccさんのブログに寄ると、
Taskのinternalなメソッドに依存して最適化が施されているので、外部からはどうしても非効率的なWhenAllしか作れない仕様になっています(クソですね!)。
とのことです。
悲しいですね。
非同期ラムダ式
asyncを付けたラムダ式を非同期ラムダ式と呼びます。
myButton.Click += async ( sender, e ) =>
{
// なんか処理
await Task.Delay( 3000 );
myTextBox.Text = "OK";
};
注意点
async/await
は実は必ずしも非同期実行にはなりません。Taskクラスの値をawaitする際に、タスクが既に完了している可能性があるからです。
またTask自体もスレッドの存在を意識しない、「同期的な処理のまとまり」とだけみなされているため、どのスレッドが対象のタスクを実行するかは定められていません。TaskクラスでのRun()等は可能な限り同じスレッドを使いまわそうとします(スレッドプール上で動作)。
その他
Awaitableパターン
Awaitable
パターンを満たしていれば、Task型以外の型をawait
できるようになります。await
したいクラスにGetAwaiter
拡張メソッドを実装すれば良いようです(詳細は省略)。
例外
async/await
がキャンセルされた場合には、awaitされた元にOperationCanceledException
と言う特別な例外が投げられます。
最後に
本記事を書くことでasync/await
を少し理解できたような気がします。ただ、実際の使い心地的なところやパフォーマンス面ではUniRxを使うのが主流となっているようです。今回はUniRxについては触れませんでしたが、また色々調べて使ってみた記事などあげ、非同期完全理解に近づけたらなと思います。それでは有難うございました。