1.21 jigowatts

Great Scott!

C# 非同期処理 コンソールでくるくるローディングアニメーションを表示する

概要

コンソールで実行中を表すローディングアニメーションを実装してみます。

ローディングアニメーションの実装

こんなやつです。
f:id:sh_yoshida:20160407174457g:plain

この処理はコンソールに出力する機能を用意して、

public static class Spiner
{
    public static void Spin()
    {
        Console.CursorVisible = false;
        char[] bars = { '/', '-', '|' };

        foreach (var item in bars)
        {
            Console.Write("Processing... {0}", item);
            Console.SetCursorPosition(0, Console.CursorTop);
            System.Threading.Thread.Sleep(120);
        }
        Console.CursorVisible = true;
    }
}

ループで回してます。

while (true)
{
    Spiner.Spin();    
}

非同期処理の実装

バーをくるくる回している裏で処理を走らせるにはタスクを使いました。はじめての非同期プログラミングどきどき…。

■ Calculation.cs
処理自体はランダムで生成した数値を2倍する処理のあと、さらにその結果を3倍した値を取得するといったあまり意味のないものです。
.NET Framework 4縛りなのでasync/awaitキーワードは使えません('Д')ノ
あと、一つ目のタスク(TaskA)で例外がスローされても、継続タスクは実行されてしまうためTask.Exceptionプロパティを参照して例外がなければ二つ目のタスク(TaskB)を実行するようにしてます。

public static class Calculation
{
    public static Task<int> Run()
    {
        //0以上10未満のランダム数値を生成
        var seed = new Random().Next(10);
        Console.WriteLine("[{0}] Random seed {1}", DateTime.Now.ToString("HH:mm:ss.fff"), seed);

        Task<int> t = Task.Factory.StartNew(() => TaskA(seed))
            .ContinueWith(c =>
            {
                if (c.Exception != null)
                {
                    //AggregateExceptionにラップされているので元の例外を再スロー
                    throw c.Exception.InnerException;
                }

                var x = TaskB(c.Result);
                return x;
            });

        //タスクの完了をポーリング
        while (!t.IsCompleted)
        {
            Spiner.Spin();
        }

        return t;
    }

    //引数を2倍し結果を返す
    public static int TaskA(int value)
    {
        Console.WriteLine("[{0}] Task A", DateTime.Now.ToString("HH:mm:ss.fff"));
        Thread.Sleep(2500);
        //エラー①
        //throw new InvalidOperationException("タスクAで発生した例外");
        var ret = value * 2;
        Console.WriteLine("[{0}] Task A result:{1}", DateTime.Now.ToString("HH:mm:ss.fff"), ret);
        return ret;
    }

    //引数を3倍し結果を返す
    public static int TaskB(int value)
    {
        Console.WriteLine("[{0}] Task B", DateTime.Now.ToString("HH:mm:ss.fff"));
        Thread.Sleep(2500);
        //エラー②
        //throw new InvalidOperationException("タスクBで発生した例外");
        var ret = value * 3;
        Console.WriteLine("[{0}] Task B result:{1}", DateTime.Now.ToString("HH:mm:ss.fff"), ret);
        return ret;
    }
}

■ Program.cs
呼び出し側ではtry-catchでTask.WaitメソッドかResultプロパティの読み取りを囲って例外をキャッチできるようにしておきます。

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Console.WriteLine("[{0}] Start", DateTime.Now.ToString("HH:mm:ss.fff"));
            var t = Calculation.Run();

            Console.Write(new string(' ', Console.WindowWidth));
            Console.SetCursorPosition(0, Console.CursorTop - 1);
            //WaitメソッドかResultプロパティの読み取りで例外をキャッチ
            Console.WriteLine("[{0}] Result:{1}", DateTime.Now.ToString("HH:mm:ss.fff"), t.Result);
        }
        catch (AggregateException e)
        {
            Console.WriteLine("[{0}] error - {1}", DateTime.Now.ToString("HH:mm:ss.fff"), e.InnerException.Message);
            Console.WriteLine("Application exit.");
            Console.ReadKey(true);
            Environment.Exit(1);
        }
        catch (Exception e)
        {
            Console.WriteLine("[{0}] error - {1}", DateTime.Now.ToString("HH:mm:ss.fff"), e.Message);
            Console.ReadKey(true);
            Environment.Exit(1);
        }

        Console.Write(new string(' ', Console.WindowWidth));
        Console.SetCursorPosition(0, Console.CursorTop - 1);
        Console.Write("[{0}] Complete!", DateTime.Now.ToString("HH:mm:ss.fff"));

        Console.ReadKey(true);
    }
}

単体テスト

非同期処理はテストしにくいということで、TaskAとTaskBメソッドを用意し非同期処理に依存しないようにしました。こうしておけば単体テストコードは簡単ですね。

[TestMethod]
public void TaskATest_Normal_引数が2のときに4が返ること()
{
    int value = 2;
    int expected = 4;
    int actual;
    actual = Calculation.TaskA(value);
    Assert.AreEqual(expected, actual);
}

[TestMethod]
public void TaskBTest_Normal_引数が2のときに6が返ること()
{
    int value = 2;
    int expected = 6;
    int actual;
    actual = Calculation.TaskB(value);
    Assert.AreEqual(expected, actual);
}

実行結果

くるくるを描画しつつ、裏で計算してます。
f:id:sh_yoshida:20160407191304g:plain
TaskAとTaskBのInvalidOperationExceptionをコメントアウトすると例外がスローされてちゃんとキャッチできてるので大丈夫だと思うんですが、非同期処理難しいですね\(^o^)/

参考