C# 非同期処理 コンソールでくるくるローディングアニメーションを表示する
ローディングアニメーションの実装
こんなやつです。
この処理はコンソールに出力する機能を用意して、
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); }
実行結果
くるくるを描画しつつ、裏で計算してます。
TaskAとTaskBのInvalidOperationExceptionをコメントアウトすると例外がスローされてちゃんとキャッチできてるので大丈夫だと思うんですが、非同期処理難しいですね\(^o^)/
参考
- 作者: 山本康彦
- 出版社/メーカー: 技術評論社
- 発売日: 2013/07/20
- メディア: 大型本
- この商品を含むブログ (12件) を見る