避免冗余异步计算

时间:2013-03-13 21:33:33

标签: c# .net task async-await

我有一些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更新) - > (更新)。这意味着最终的UI显示陈旧状态(反映A,但不是B)。
  2. 如果我总是先等待先前的UpdateStatusAsync完成(我目前正在做的事情),我可以多次执行昂贵的状态计算。理想情况下,我只需要对一系列更新进行“最后”计算。
  3. 我正在寻找的是一个干净的模式,可以实现以下目标:

    1. 最终状态永远不应该是“陈旧”超过一小段时间(即我不希望UI与基础状态不同步)
    2. 如果在短时间内发生多个更新呼叫(常见用例),我宁愿避免重复工作,而是始终计算“最新”更新。
    3. 由于有几种情况可能会在非常接近的情况下(即在几毫秒内)发生多次更新,因此有一种方法可以避免在其他更新请求进入的情况下短时间内开始处理。
    4. 看起来这应该是一个相当普遍的问题,所以我想我会问这里是否有人知道这样做的特别干净。

4 个答案:

答案 0 :(得分: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 创建(甚至)更多垃圾,这种简单方法将始终取消较旧的状态更新,如果没有' ta" break"在这些事件中,用户界面最终可能会落后。

这种复杂程度是async开始达到极限的地方。 Reactive extensions如果您需要更复杂的东西(例如处理"中断"那么您至少每隔一段时间就会获得一次UI更新),这将是一个更好的选择。 Rx在处理时机方面特别擅长。

答案 1 :(得分: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更新移出循环。取决于你是否介意看到中间更新。

答案 2 :(得分:0)

由于我似乎走在正确的轨道上,我会提出我的建议。在非常基本的伪代码中,看起来这可能起到了作用:

int counter = 0;

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

这样您就可以跳过任意数量太多的电话,而计数器会确保您仍然保持用户界面的最新状态。

答案 3 :(得分: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;
            }
        }
    }

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