Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Created August 21, 2018 03:31
Show Gist options
  • Save ufcpp/a72f63d11962f7a5a9a5981b6be31f74 to your computer and use it in GitHub Desktop.
Save ufcpp/a72f63d11962f7a5a9a5981b6be31f74 to your computer and use it in GitHub Desktop.
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