3

次のようなメソッドがある UI コードがあります。

    private async Task UpdateStatusAsync()
    {
        //Do stuff on UI thread...

        var result = await Task.Run(DoBunchOfStuffInBackground);

        //Update UI based on result of background processing...
    }

目標は、プロパティがその状態に影響を与えるように変更されるたびに、UI が比較的複雑な計算されたステータスを更新することです。ここにはいくつかの問題があります。

  1. ステータスを更新する各場所からこのメソッドを直接呼び出すと、最終的に更新されたステータスが正しくない可能性があります。プロパティ A が変更され、次にプロパティ B が変更されたとします。B は A の後に UpdateStatusAsync を呼び出しますが、コールバック コード (最終的な UI の更新) が逆の順序で発生することがあります。つまり、(A -> 更新) -> (B -> 更新) -> (B 更新) -> (A 更新)。これは、最終的な UI が古いステータスを示していることを意味します (A を反映していますが、B は反映していません)。
  2. 以前の UpdateStatusAsync が最初に完了するのを常に待っていると (現在行っていること)、高価な状態計算を何度も実行することになります。理想的には、一連の更新の「最後の」計算のみを行う必要があります。

私が探しているのは、次のことを実現するクリーンなパターンです。

  1. 最終ステータスが「古い」状態であってはなりません (つまり、UI が基になる状態と同期しなくなることは望ましくありません)。
  2. 短時間に複数の更新呼び出しが発生した場合 (一般的なユース ケース)、重複した作業を避け、代わりに常に「最新の」更新を計算することをお勧めします。
  3. 複数の更新が非常に近い間隔で (つまり、ミリ秒以内に) 発生する場合があるため、他の更新要求が来た場合に備えて、処理を短時間開始しないようにする方法があると便利です。

これはかなり一般的な問題のように思われるので、これを行うための特にクリーンな方法を誰かが知っているかどうかここで尋ねたいと思いました。

4

4 に答える 4

2

さて、最も簡単なアプローチは、CancellationToken古いステータスの更新をキャンセルしTask.Delay、ステータスの更新を遅らせるために使用することです。

private CancellationTokenSource cancelCurrentUpdate;
private Task currentUpdate;
private async Task UpdateStatusAsync()
{
  //Do stuff on UI thread...

  // Cancel any running update
  if (cancelCurrentUpdate != null)
  {
    cancelCurrentUpdate.Cancel();
    try { await currentUpdate; } catch (OperationCanceledException) { }
    // or "await Task.WhenAny(currentUpdate);" to avoid the try/catch but have less clear code
  }

  try
  {
    cancelCurrentUpdate = new CancellationTokenSource();
    var token = cancelCurrentUpdate.Token;
    currentUpdate = Task.Run(async () =>
    {
      await Task.Delay(TimeSpan.FromMilliseconds(100), token);
      DoBunchOfStuffInBackground(token);
    }, token);

    var result = await currentUpdate;

    //Update UI based on result of background processing...
  }
  catch (OperationCanceledException) { }
}

ただし、更新が非常に速い場合この種のアプローチではGCのガベージが(さらに)多くなり、この単純なアプローチでは常に古いステータスの更新がキャンセルされるため、イベントに「中断」がない場合、UIが遅れてしまいます。

このレベルの複雑さはasync、限界に達し始めるところです。より複雑なものが必要な場合は、リアクティブ拡張機能の方が適しています(たとえば、その「中断」を処理して、少なくとも1つのUI更新を頻繁に取得するなど)。Rxはタイミングの処理に特に優れています。

于 2013-03-13T22:38:24.817 に答える
1

タイマーを使用せずにこれを実行できるはずです。一般に:

private async Task UpdateStatusAsync()
{
    //Do stuff on UI thread...

    set update pending flag

    if currently doing background processing
    {
        return
    }

    while update pending
    {
        clear update pending flag
        set background processing flag
        result = await Task.Run(DoBunchOfStuffInBackground);
        //Update UI based on result of background processing...
    }
    clear background processing flag
}

async/awaitのコンテキストでこれらすべてを正確に行う方法を考えなければなりません。私はBackgroundWorker過去に似たようなことをしたので、それが可能であることを知っています。

更新の欠落を防ぐのは非常に簡単ですが、不必要なバックグラウンド処理が行われる場合があります。しかし、10回の更新が短期間で投稿された場​​合、9回の不要な更新を実行することは確かに排除されます(おそらく、最初と最後を実行するだけです)。

必要に応じて、UIの更新をループの外に移動できます。中間の更新を確認するかどうかによって異なります。

于 2013-03-13T22:38:47.210 に答える
0

私は正しい方向に進んでいるようですので、提案を提出します。非常に基本的な擬似コードでは、これでうまくいくようです。

int counter = 0;

if (update received && counter < MAX_ITERATIONS)
{
     store info;
     reset N_MILLISECOND timer;
}
if (timer expires)
{
    counter = 0;
    do calculation;
}

これにより、互いに近すぎる呼び出しを必要なだけスキップできますが、カウンターはUIを最新の状態に保つことを保証します。

于 2013-03-13T22:22:38.930 に答える