【问题标题】:Avoiding redundant asynchronous calcluations避免多余的异步计算
【发布时间】:2013-03-13 21:33:33
【问题描述】:

我有一些 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. 由于多个更新可能发生在非常接近(即毫秒内)的几种情况下,如果有其他更新请求进来,有一种方法可以避免在短时间内启动处理会很方便。

这似乎应该是一个相当普遍的问题,所以我想我会在这里问是否有人知道这样做的特别干净的方法。

【问题讨论】:

  • 这不就是这么简单:if (update received) { store info; reset 100ms timer }; if (timer expires) { do calculation };?
  • @Scott,这样的事情可能会奏效,尽管它可能会变得非常陈旧(例如,我可以每 10 毫秒获得更新 1 秒,在这种情况下,它会在 UI 之前花费整整一秒status 开始更新。)我试图在避免冗余计算和保持 UI 最新之间找到平衡。
  • 如果你想保持这个总体思路,只需增加一点复杂性:`if (update received && counter 太过时。
  • 好吧,如果您使用事件(甚至是INotifyPropertyChanged 事件),您可以查看Reactive Extensions,它具有节流、事件流的组合等等。只是一个想法。

标签: c# .net task async-await


【解决方案1】:

嗯,最直接的方法是使用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 开始达到极限的地方。 Reactive extensions 如果您需要更复杂的东西(例如处理“中断”以便您至少每隔一段时间获得一次 UI 更新),那将是一个更好的选择。 Rx 特别擅长处理时间问题。

【讨论】:

    【解决方案2】:

    您应该能够在不使用计时器的情况下执行此操作。一般来说:

    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 更新移出循环。取决于您是否介意看到中间更新。

    【讨论】:

    • 我最终使用了类似于这种方法的方法(添加了一个计时器来延迟启动任务。)
    【解决方案3】:

    既然看来我走在正确的轨道上,我将提交我的建议。在非常基本的伪代码中,看起来这样可以解决问题:

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

    这将使您可以根据需要跳过尽可能多的彼此距离太近的呼叫,而计数器将确保您仍使 UI 保持最新状态。

    【讨论】:

      【解决方案4】:

      我最终使用了 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;
                  }
              }
          }
      

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-09-16
        • 1970-01-01
        • 2017-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多