LoginSignup
3
2

More than 5 years have passed since last update.

(多分)東邦随一のIx Async歩き回り

Last updated at Posted at 2015-02-07

そもそもなんなのか?

Microsoftが提供しているIxファミリの1つ。
個人的な見解では、Enumerable/Enumeratorの対を非同期化して別の何かにしてみた感じ。

前準備

Nugetにて、”Ix-Async”を参照すれば使えます。

使い方

事前の想定

System.Int32を引数にとって、System.Int32を返す結構重めの処理があったと想定して、以下のメソッドを用意しています。


private static int SomeHeavyProcess(int value)
{
    //重い処理の変わりにてけとーに遅延を挟む。(500ms)
    Task.Delay(500).Wait();
    //適当に返す
    return value*2;
}

前座:同期的処理でやってみる

上記メソッドをたとえば、0~10の値を適用した上で、その合計と平均を出力する場合、おおむね以下の通りとなる。


static void SyncProcess()
{
    var sum = Enumerable.Range(0, 10).Select(SomeHeavyProcess).Sum();
    Console.WriteLine("Summation is " + sum);
    Console.WriteLine("Average is " + (sum/10.0));
}

ただ、GUIを使ってこんな書き方したら、5秒間もUIスレッドをブロックする残念実装になる。
また、CUIにおいても、たとえばAAを使った、プログレスやら、実行中のアニメを挟めないのでやっぱり残念実装になる。

二ツ目:とりま書いてみる

前座の処理をIx Asyncを使って書いてみると、おおむね以下の通り。


private static void UseIsAsync()
{
    //Enumerable.Rangeぢゃなくて、AsyncEnumerable.Rangeを使う。
    var parameters = AsyncEnumerable.Range(0, 10);

    //Taskにくるまって結果が戻ってくる。
    var sum = parameters.Select(SomeHeavyProcess).Sum();

    //CUIなので、お気楽に待つ(ポーリングする)
    for (;;)
    {
        Console.SetCursorPosition(0, 0);
        Console.Write('-');
        Task.Delay(100).Wait();
        Console.SetCursorPosition(0, 0);
        Console.Write('/');
        Task.Delay(100).Wait();
        Console.SetCursorPosition(0, 0);
        Console.Write('|');
        Task.Delay(100).Wait();
        Console.SetCursorPosition(0, 0);
        Console.Write('\\');
        Task.Delay(100).Wait();
        Console.SetCursorPosition(0, 0);
        Console.Write('-');
        Task.Delay(100).Wait();


        //あんまり良くないポーリングの実例(ポージングそのものが実はあんまり良くないけど)
        if(sum.Status== TaskStatus.RanToCompletion) break;
    }

    Console.Clear();
    Console.WriteLine("Summation is " + sum.Result);
    Console.WriteLine("Average is " + (sum.Result/10.0));

}


Sumメソッドの適用結果がTaskになってやってくるのがミソ。
コレなら待機可能でしょっ?て感じ。

くいつき:async/awaitとともに使う

先の例だと、Sumの結果がTaskに詰まってやってきてる。コレは当然のことながらawaitableなので、async/awaitの中で以下のように利用できちゃったりする。


private static async Task<Tuple<int,double>> UseAsyncEnumerable()
{
    var parameters = AsyncEnumerable.Range(0, 10);
    var sum = await parameters.Sum();
    return Tuple.Create(sum, (sum/10.0));
}

実際、私感だけど、こいつらのレゾンテートルはまさにここいらへんにあるんじゃ無いかなって思ってる。1

ひざがわり:結局こいつら何なの?

ざっと使ってみたので、少し追っかけてみましょってことで。。。
AsyncEnumerable.Rangeの返す代物がなんなのか見てみると、IAsyncEnumerableが返ってくる。これは、Enumerable.RangeがIEnumerableを返し、Observable.Rangeが、IObservableを返すのととってもとっても似てる。
じゃあ、IasyncEnumerableってなんなのさ?っていうと、IEnumerableととてもよく似てるけど決定的に違う何か。よく似ている理由は、GetEnumeratorメソッドを持っていること。決定的に違うのは当該メソッドの戻り値がIEnumeratorじゃなくて、IAsyncEnumeratorになっている点。
んだばIAsyncEnumeratorって何なのさ?っていうと、コレもこれで、IEnumeratorととってもよく似てるけど決定的に違う何か。よく似ている理由は、

  • IDisposableだよ!
  • MoveNextメソッドを持ってるよ!
  • Currentプロパティも完備!
  • MoveNext読んでからじゃないとCurrentプロパティ読んでも意味ないよ!

じゃあ何が違うかというと

  • 時代の遺物たるResetメソッドはないよ 2 
  • MoveNextの戻り値はboolじゃなくて、Taskだよ!
  • 待機完了後にCurrent読まないとダメだよ
  • foreachで回せないよ! 3

という感じ。
foreachで回せないのはぱっと見激痛だけど、実際は拡張メソッドにForeEachメソッドがあるので、そこまで実際問題にはならなかったりする。ただ、普通に下記のように書いた場合、同期的な実行と同じになっちゃうので、その点は注意。

        private static void Main(string[] args)
        {
            //同期的実行になってあんまりおいしくない。
            AsyncEnumerable.Range(0, 10).Select(SomeHeavyProcess).ForEach(Console.WriteLine);
        }

それじゃ、ってことでForEachもLinqもなしに、合計を出したい場合、どーするか?と言うと、こんな感じ。
コレを見ていただければ何がどーなってるのか、あらかたご理解いただけるかと。。。


private static void Main(string[] args)
{
    var result = AsyncEnumerable.Range(0, 10).Select(SomeHeavyProcess);
    var accum = 0;

    using (var iterator = result.GetEnumerator())
    {
        for (;;)
        {
            //Task<bool>貰ってくる。
            var cond = iterator.MoveNext();
            //しばし待つ!
            cond.Wait();

            if (cond.Result) accum += iterator.Current;
            else break;
        }
    }


    Console.WriteLine("Summation is " + accum);
    Console.WriteLine("Average is " + (accum/10.0));
}

注意するべきは、非同期であって並行ではないという点。
MoveNextをガンガン回して斉時的に結果を得ようとしても結果は酷いことになる。
例えば下記みたいなコトはダメ、絶対


        private static void Main(string[] args)
        {
            var seq = AsyncEnumerable.Range(0, 10).Select(x =>
            {
                var ret = SomeHeavyProcess(x);
                //行儀が悪いけど、どー鳴ってるか内部状態を書き出す。
                Console.WriteLine(ret);
                return (object) null;
            });

            var iterator = seq.GetEnumerator();

            for (var i = 0; i < 10; i++)
            {
                //とりあえず待機無しで10回ぶん回してみる。
                iterator.MoveNext();
            }


            Console.ReadLine();

            //結果は惨憺たる状態
            //10
            //18
            //4
            //16
            //10
            //10
            //12
            //4
            //12
            //14
        }

トリ:まとめ

非同期にIEnumerable/IEnumerator、そしてLINQを構築すると、どんな世界になる?
てコトの解の一つじゃないかなと言うのが実感。
ただ、なまじ、IEnumerable/IEnumeratorと酷似しすぎているのに何か違っていて結構混乱してしまうって点と、根本論としてこの手扱うならRxの方がベストじゃね?っていうオチ。んでもって、とどめに、async/awaitを使うと、IEnumerable/IEnumeratorの世界でも普通に非同期化できてしまうので、そー言う面においては、わりかし微妙な立ち位置になってるかな?と、個人的に思ったり思わなかったりしますです。えぇ。

参考文献


  1. mappingを非同期実行できるというかそんなかんじ? 

  2. こいつは元々COMとの兼ね合いだったし、yield使うと例外が飛んでくる程度にいらない子 

  3. 先の通りMoveNext()がbool返してくれないから使えない。 

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2