MulticastDelegate に連結した非同期メソッドに対して、全部の完了を await したいときの処理
using System; | |
using System.Runtime.CompilerServices; | |
using System.Threading.Tasks; | |
// デモ用。 | |
// 引数で与えた時間だけ Delay した後、Console.WriteLine するだけのクラス。 | |
class Sample | |
{ | |
TimeSpan _delayInterval; | |
public Sample(int delayIntervaleMillisecondes) : this(TimeSpan.FromMilliseconds(delayIntervaleMillisecondes)) { } | |
public Sample(TimeSpan delayInterval) => _delayInterval = delayInterval; | |
public async Task X() | |
{ | |
await Task.Delay(_delayInterval); | |
Console.WriteLine(_delayInterval); | |
} | |
} | |
class Program | |
{ | |
static async Task Main() | |
{ | |
// デリゲートを1個用意して、 | |
Func<Task> f = null; | |
// += で10個ほど非同期メソッドを連結。 | |
var r = new Random(); | |
for (int i = 0; i < 10; i++) | |
{ | |
var x = new Sample(r.Next(30, 500)); | |
f += x.X; | |
} | |
// InvokeAll を利用 | |
Console.WriteLine("await InvokeAll(f)"); | |
await InvokeAll(f); | |
// ちゃんと10個全部のタスクが完了するまで待つ。 | |
// 参考: 普通に f() で呼ぶと… | |
Console.WriteLine("await f()"); | |
await f(); | |
// 戻り値がある MulticastDelegate を呼ぶと、最後の1個の戻り値だけが取れる。 | |
// その結果、最後の1個しか await できない。 | |
// 今回の実装だと、最後の1個よりも delay が短い奴だけが WriteLine で表示される。 | |
// 乱数で delay を決めてるので、呼ぶたびに表示される数が変わる。 | |
} | |
/// <summary> | |
/// <see cref="MulticastDelegate"/> になってる <see cref="Func{Task}"/> に対して、 | |
/// <see cref="MulticastDelegate.GetInvocationList"/> 全部を呼び出し → その戻り値全部を <see cref="Task.WhenAll(Task[])"/>。 | |
/// </summary> | |
/// <param name="multicastDelegate"></param> | |
/// <returns></returns> | |
static Task InvokeAll(Func<Task> multicastDelegate) | |
{ | |
// 補足: | |
// MulticastDelegate は内部的に object[] で invocation list を持ってる。 | |
// GetInvocationList を呼ぶと、new Delegate[] してそこに list をコピーしてたり。 | |
// もったいない実装… | |
var dlist = multicastDelegate.GetInvocationList(); | |
// Delegate[] で帰ってくる戻り値には、ちゃんと Func<Task> な要素が入ってる。 | |
// ((Func<Task>)list[i])() でちゃんと呼べる。 | |
// ただ、キャストは内部的に型チェックがかかってて、多少のオーバーヘッドがあるみたい。 | |
// 絶対に Func<Task> しかないっていない保証がある Delegate に対して型チェックのコストを掛けたくないとき、 | |
// 以下のようなバッドノウハウがある。 | |
// Unsafe.As で無理やり Wrapper[] に変換。(Wrapper は Func<Task> を1個だけ持つ構造体。) | |
// Unsafe.As は型チェックをすっ飛ばして型変換してしまう、名前通り unsafe なメソッド。 | |
// JIT 時に「構造体の配列なので variance は起こらないはず」という最適化が掛かるらしく、Func<Task>[] に Unsafe.As するよりも Wrapper を介する方が速いらしい。 | |
// 参考: https://github.com/dotnet/corefx/issues/23689 | |
var list = Unsafe.As<Delegate[], Wrapper[]>(ref dlist); | |
// 呼び出して、Task[] で受け取る。 | |
var tasks = new Task[list.Length]; | |
for (int i = 0; i < list.Length; i++) | |
{ | |
tasks[i] = list[i].Value(); | |
} | |
// それを WhenAll。 | |
return Task.WhenAll(tasks); | |
} | |
private struct Wrapper { public Func<Task> Value; } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment