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开始达到极限的地方。如果您需要更复杂的东西(例如处理“中断”,以便您至少每隔一段时间获得一次 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 回答
0

我最终使用了 Jim Mischel 推荐的方法,并添加了一个计时器来聚合快速传入的触发器:

public sealed class ThrottledTask
    {
        private readonly object _runLock = new object();
        private readonly Func<Task> _runTask;
        private Task _loopTask;
        private int _updatePending;

        public ThrottledTask(Func<Task> runTask)
        {
            _runTask = runTask;
            AggregationPeriod = TimeSpan.FromMilliseconds(10);
        }

        public TimeSpan AggregationPeriod { get; private set; }

        public Task Run()
        {
            _updatePending = 1;

            lock (_runLock)
            {
                if (_loopTask == null)
                    _loopTask = RunLoop();

                return _loopTask;
            }
        }

        private async Task RunLoop()
        {
            //Allow some time before we start processing, in case many requests pile up
            await Task.Delay(AggregationPeriod);

            //Continue to process as long as update is still pending
            //This clears flag on each cycle in a thread-safe way
            while (Interlocked.CompareExchange(ref _updatePending, 0, 1) == 1)
            {
                await _runTask();
            }

            lock (_runLock)
            {
                _loopTask = null;
            }
        }
    }

一旦聚合期过去,只要仍有传入的触发器,它就会尽可能快地运行更新。关键是,如果触发器发生的速度比计算快,这不会叠加冗余更新,并且它始终确保“最后一个”触发器得到更新。

于 2013-06-04T23:21:23.953 回答