[tweet https://twitter.com/biac/status/317963665636732928 ]
を見て Task.Yield() を早速試してみました。ループ待ちとしては、連続したアニメーションをつなぐために、await/async を使う のパターンになるかなと。
■Application.DoEvents はどう実装するのか?
かつて、VB では画面の更新に DoEvents を使っていたわけで、これが結構便利。プログラムの for ループの中に DoEvents を差し込めば、画面(UI)を再描画してくれるのでループ内のカウンタ表示とか、キャンセルボタンを押すために DoEvents を入れたものです。似たパターンでは、VC++ でも Message ループを差し込んだり、VB.NET で Appliction.DoEvents を入れていました。
が、ストアアプリの場合は、この便利な DoEvents がないのです(確かに、WPFのほうにもないですね)。これをどう実現するのか?ってのが WPF 時代の人は知っているのでしょうが(あまり WPF はハードにタッチしていないので)、.NET 4.5 の場合は、async/await と Task を使いましょう、ってのが本来の姿。長いループの場合は、適宜 Task にして事項しましょう、ってのが推奨パターンです。
が、いちいち Task にするのが面倒臭いのと、Task の中から UI を触れないので Task の中では UI に対して Invoke をするか、ディスパッチをするか、というちょっと面倒なことをしないといけません(多分、まとめたページが世の中に既にあるはず)。
こんな風に、
- Task.Yeild を使うパターン
- Task.Delay を使うパターン
- Task を使うパターン
の三種類で試してみます。いわゆる「重たい処理」というのが、アニメーションの完了待ちになっているので実は重い処理とは違うので、コードがちょっと違いますが、まあ応用が利くかと。
■Task.Yeild 待ちのパターン
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private async void Button_Click_1( object sender, RoutedEventArgs e) { bool completed = false ; sbMove.Completed += (_, __) => { completed = true ; }; text1.Text = "アニメーション開始" ; int i = 0; sbMove.Begin(); while ( completed == false ) { count1.Text = i.ToString(); await Task.Yield(); i++; } text1.Text = "アニメーション終了" ; sbMove.Stop(); } |
開始と終了とにメッセージを TextBlock に表示します。また、ループの間にカウンタを表示。アニメーションの完了待ち(Completed イベント待ち)という「軽い処理」をしてるので、この Task.Yield() の処理は相当重たくなっています。実際に「重たい処理」が入れば、即時戻ってくる Task.Yield() はそれなりに使いでがあるかと。
■Task.Delay 待ちのパターン
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private async void Button_Click_2( object sender, RoutedEventArgs e) { bool completed = false ; sbMove.Completed += (_, __) => { completed = true ; }; text1.Text = "アニメーション開始" ; int i = 0; sbMove.Begin(); while (completed == false ) { count2.Text = i.ToString(); await Task.Delay(100); // ちょっとだけ待つ i++; } text1.Text = "アニメーション終了" ; sbMove.Stop(); } |
一見すると、Task.Yield() と変わらないように見えますが、Delay メソッドでミリ秒だけ待ちます。Delay メソッドは非同期(UIスレッドに処理を戻す)仕様なので、await を付けて結果的に Yield メソッドと同じ結果を得られます。本当に重たい処理をする場合には、Task.Delay(1); のように瞬時だけ処理を戻すというパターンでも使えます。
後で実験してみますが、Yeild よりも Delay のほうが処理が軽くなります。
■別 Task にするパターン
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private async void Button_Click_3( object sender, RoutedEventArgs e) { sbMove.Completed += (_, __) => { _completed = true ; }; sbMove.Begin(); text1.Text = "アニメーション開始" ; await WaitComplete(); // 完了待ち text1.Text = "アニメーション終了" ; sbMove.Stop(); } bool _completed = false ; /// /// 完了待ち /// /// private Task WaitComplete() { return Task.Run( async () => { while (_completed == false ) { await Task.Delay(100); // ちょっと待つ // ここをコメントアウトしてもスムースに動く } }); } |
おそらくこれが正式な方法で(もう少し良いパターンがあるような気もしますが)、完了待ちを別のタスクにします。_completed がグローバル扱いになるのが難点ですが、これはひと工夫(refを使うとか)すれば消えるはずです。WaitComplete メソッド内のタスクは UI とは別スレッドのために UI にあるカウンタの更新はできません。なので、ちょっとした小細工をするときには面倒といえば面倒ですね。
ただし、このコードの利点としては、await Task.Delay(100); の部分をコメントアウトして、ループだけにしても他のアニメーションがスムースに動くということです。これは async/await の戻りが UI スレッドではなくて、ワーク用のスレッドに戻るからなんですね。なので UI スレッドに負担を掛けずにすみます。なので、uI の応答性を重要視する場合はこのパターンで書くことになるでしょう。
■実測してみる。
Yeild と Delay の UI スレッドへの負担を見てみると。
アニメーションが 5秒の場合には、Task.Yeild は 7万回以上呼び出されているのに対して、Task.Delay は 44回で済みます。Delay が 50回呼び出されていないのは、まあ誤差の範囲で。
ちなみに、シミュレーターの場合は Task.Yeild では動きがかくかくしていますが、ローカルコンピューターではスムースに動いています。でもまあ、5秒間に7万回呼び出しても仕方がないわけで、CPU を喰わない(省電力化)をするためも、Delay のほうがお得かという結論です。
サンプルコードはこちら。
http://sdrv.ms/10mK1ch
自分で見てもなぜ Yeild と比較しているのか分からないが、目的としては複数の storyboard をつなげる時にアニメーションの完了待ちをする必要があったので、これをシーケンシャルに書きたかったため Task.Yeild を調べてみた。
IEnumable/Yeild でつなげるよりも、await の連続のほうがシーケンスとして解りやすいのでは?という話。結局のところはどちらでもよいのだが。
ちなみにアニメーションの完了待ち&連続動作としては Task.WaitAny を使ったほうが適切かなと思ったり。後で書き換えてみよう。
cheap Colts jersey are at super low price and good quality
authentic jerseys china http://aquarius.com.sg/js/jqueries.html