Created
August 21, 2018 03:31
-
-
Save ufcpp/a72f63d11962f7a5a9a5981b6be31f74 to your computer and use it in GitHub Desktop.
MulticastDelegate に連結した非同期メソッドに対して、全部の完了を await したいときの処理
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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