[WinRT] ストアアプリで DoEvents を実現する

[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 待ちのパターン

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 待ちのパターン

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 にするパターン

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

zip ファイル

SampleDoEvents-v1.0-src.zip

カテゴリー: C#, WinRT パーマリンク

[WinRT] ストアアプリで DoEvents を実現する への2件のフィードバック

  1. masuda のコメント:

    自分で見てもなぜ Yeild と比較しているのか分からないが、目的としては複数の storyboard をつなげる時にアニメーションの完了待ちをする必要があったので、これをシーケンシャルに書きたかったため Task.Yeild を調べてみた。
    IEnumable/Yeild でつなげるよりも、await の連続のほうがシーケンスとして解りやすいのでは?という話。結局のところはどちらでもよいのだが。
    ちなみにアニメーションの完了待ち&連続動作としては Task.WaitAny を使ったほうが適切かなと思ったり。後で書き換えてみよう。

  2. authentic jerseys china のコメント:

    cheap Colts jersey are at super low price and good quality
    authentic jerseys china http://aquarius.com.sg/js/jqueries.html

コメントは停止中です。