C#:Task.Run() に渡すメソッドも(できれば)async にしよう
Task.Run()
は .NET において指定した処理をスレッドプール上で実行させるメソッドです。元のスレッドをブロックせずに処理を実行させることができるため、重めの処理を別のスレッドに逃がしたいときによく使います。
Console.WriteLine("Start");
await Task.Run(() =>
{
HeavyAndBlockingMethod();
File.WriteAllBytes("very_large_file.bin", data);
HeavyAndBlockingMethod();
});
Console.WriteLine("End");
ただし、スレッドプール上でブロッキング処理を行うのは注意が必要です。スレッドプールが保持するスレッドの数は有限なので、すべてのスレッドを重めのタスクが占有すると新たなタスクが開始できなくなり、結果として全体のスループットが下がる可能性があります。メインスレッドでもスレッドプールでもブロッキング処理は可能な限り避けたいということですね。
上のコードでは HeavyAndBlockingMethod()
はブロッキングにならざるを得ない状況を想定していますが、File.WriteAllBytes()
には非同期バージョンの File.WriteAllBytesAsync()
が存在しており、原理的にはノンブロッキングな処理が可能なはずです。
実は、Task.Run()
には Func<Task>
を受け取るオーバーロードが存在します。
Console.WriteLine("Start");
await Task.Run(async () =>
{
HeavyAndBlockingMethod();
await File.WriteAllBytesAsync("very_large_file.bin", data);
HeavyAndBlockingMethod();
});
Console.WriteLine("End");
これによって、少なくともファイル書き込みの時間に関してはスレッドプール上のスレッドをブロックせず、その間に別のタスクを差し込むことができるようになります。このオーバーロードのうれしいポイントは、Task.Run()
自体の戻り値である Task
が、ちゃんと引数として渡した非同期処理の完了を待ってくれるという点ですね。
もちろん依然として HeavyAndBlockingMethod()
はブロッキングのままですが、スレッドプールはより有効に活用されるようになり、全体としてパフォーマンスの向上に寄与します。Task.Run()
に渡すメソッドは部分的にでも async / await を活用するのがおすすめです。
Discussion